netbox-atlassian 0.2.1__tar.gz → 0.2.4__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 (23) hide show
  1. {netbox_atlassian-0.2.1 → netbox_atlassian-0.2.4}/PKG-INFO +7 -1
  2. {netbox_atlassian-0.2.1 → netbox_atlassian-0.2.4}/README.md +6 -0
  3. netbox_atlassian-0.2.4/netbox_atlassian/__init__.py +148 -0
  4. {netbox_atlassian-0.2.1 → netbox_atlassian-0.2.4}/netbox_atlassian/atlassian_client.py +32 -26
  5. netbox_atlassian-0.2.4/netbox_atlassian/templates/netbox_atlassian/device_tab.html +20 -0
  6. netbox_atlassian-0.2.4/netbox_atlassian/templates/netbox_atlassian/endpoint_tab.html +25 -0
  7. netbox_atlassian-0.2.1/netbox_atlassian/templates/netbox_atlassian/device_tab.html → netbox_atlassian-0.2.4/netbox_atlassian/templates/netbox_atlassian/tab_content.html +0 -3
  8. netbox_atlassian-0.2.4/netbox_atlassian/templates/netbox_atlassian/vm_tab.html +20 -0
  9. netbox_atlassian-0.2.4/netbox_atlassian/urls.py +30 -0
  10. {netbox_atlassian-0.2.1 → netbox_atlassian-0.2.4}/netbox_atlassian/views.py +237 -37
  11. {netbox_atlassian-0.2.1 → netbox_atlassian-0.2.4}/netbox_atlassian.egg-info/PKG-INFO +7 -1
  12. {netbox_atlassian-0.2.1 → netbox_atlassian-0.2.4}/netbox_atlassian.egg-info/SOURCES.txt +2 -0
  13. {netbox_atlassian-0.2.1 → netbox_atlassian-0.2.4}/pyproject.toml +1 -1
  14. netbox_atlassian-0.2.1/netbox_atlassian/__init__.py +0 -74
  15. netbox_atlassian-0.2.1/netbox_atlassian/templates/netbox_atlassian/vm_tab.html +0 -178
  16. netbox_atlassian-0.2.1/netbox_atlassian/urls.py +0 -17
  17. {netbox_atlassian-0.2.1 → netbox_atlassian-0.2.4}/netbox_atlassian/forms.py +0 -0
  18. {netbox_atlassian-0.2.1 → netbox_atlassian-0.2.4}/netbox_atlassian/navigation.py +0 -0
  19. {netbox_atlassian-0.2.1 → netbox_atlassian-0.2.4}/netbox_atlassian/templates/netbox_atlassian/settings.html +0 -0
  20. {netbox_atlassian-0.2.1 → netbox_atlassian-0.2.4}/netbox_atlassian.egg-info/dependency_links.txt +0 -0
  21. {netbox_atlassian-0.2.1 → netbox_atlassian-0.2.4}/netbox_atlassian.egg-info/requires.txt +0 -0
  22. {netbox_atlassian-0.2.1 → netbox_atlassian-0.2.4}/netbox_atlassian.egg-info/top_level.txt +0 -0
  23. {netbox_atlassian-0.2.1 → netbox_atlassian-0.2.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: netbox-atlassian
3
- Version: 0.2.1
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
@@ -201,6 +201,12 @@ flake8 netbox_atlassian/
201
201
 
202
202
  See [CHANGELOG.md](CHANGELOG.md) for release history.
203
203
 
204
+ ## Support
205
+
206
+ If you find this plugin helpful, consider supporting development:
207
+
208
+ [![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)
209
+
204
210
  ## License
205
211
 
206
212
  Apache 2.0
@@ -174,6 +174,12 @@ flake8 netbox_atlassian/
174
174
 
175
175
  See [CHANGELOG.md](CHANGELOG.md) for release history.
176
176
 
177
+ ## Support
178
+
179
+ If you find this plugin helpful, consider supporting development:
180
+
181
+ [![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)
182
+
177
183
  ## License
178
184
 
179
185
  Apache 2.0
@@ -0,0 +1,148 @@
1
+ """
2
+ NetBox Atlassian Plugin
3
+
4
+ Display Jira issues and Confluence pages related to devices in NetBox.
5
+ Searches by configurable fields (hostname, serial, role, etc.) with OR logic.
6
+ """
7
+
8
+ import logging
9
+
10
+ from netbox.plugins import PluginConfig
11
+
12
+ __version__ = "0.2.4"
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class AtlassianConfig(PluginConfig):
18
+ """Plugin configuration for NetBox Atlassian integration."""
19
+
20
+ name = "netbox_atlassian"
21
+ verbose_name = "Atlassian"
22
+ description = "Display Jira issues and Confluence pages related to devices"
23
+ version = __version__
24
+ author = "sieteunoseis"
25
+ author_email = "sieteunoseis@github.com"
26
+ base_url = "atlassian"
27
+ min_version = "4.0.0"
28
+
29
+ # Required settings - plugin won't load without these
30
+ required_settings = []
31
+
32
+ # Default configuration values
33
+ default_settings = {
34
+ # Jira settings (on-prem)
35
+ "jira_url": "", # e.g., "https://jira.example.com"
36
+ "jira_username": "",
37
+ "jira_password": "", # or API token
38
+ "jira_token": "", # Personal Access Token (PAT) - preferred for on-prem
39
+ "jira_verify_ssl": True,
40
+ # Confluence settings (on-prem)
41
+ "confluence_url": "", # e.g., "https://confluence.example.com"
42
+ "confluence_username": "",
43
+ "confluence_password": "", # or API token
44
+ "confluence_token": "", # Personal Access Token (PAT) - preferred for on-prem
45
+ "confluence_verify_ssl": True,
46
+ # Cloud settings (for future use)
47
+ "use_cloud": False,
48
+ "cloud_api_token": "",
49
+ "cloud_email": "",
50
+ # Search configuration
51
+ # Fields to search - values are device attribute paths
52
+ # Searches use OR logic - matches any field
53
+ "search_fields": [
54
+ {"name": "Hostname", "attribute": "name", "enabled": True},
55
+ {"name": "Serial", "attribute": "serial", "enabled": True},
56
+ {"name": "Asset Tag", "attribute": "asset_tag", "enabled": False},
57
+ {"name": "Role", "attribute": "role.name", "enabled": False},
58
+ {"name": "Primary IP", "attribute": "primary_ip4.address", "enabled": False},
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
+ ],
68
+ # Jira search settings
69
+ "jira_max_results": 10,
70
+ "jira_projects": [], # Empty = search all projects
71
+ "jira_issue_types": [], # Empty = all types
72
+ # Confluence search settings
73
+ "confluence_max_results": 10,
74
+ "confluence_spaces": [], # Empty = search all spaces
75
+ # General settings
76
+ "timeout": 30,
77
+ "cache_timeout": 300, # Cache results for 5 minutes
78
+ # Enable legacy SSL renegotiation for older servers (required for OHSU wiki)
79
+ "enable_legacy_ssl": False,
80
+ # Device type filtering (like catalyst-center)
81
+ # Empty list = show tab for all devices
82
+ "device_types": [], # e.g., ["cisco", "juniper"]
83
+ }
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
+
147
+
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,20 @@
1
+ {% extends 'dcim/device/base.html' %}
2
+ {% load helpers %}
3
+
4
+ {% block content %}
5
+ {% if loading %}
6
+ <div id="atlassian-content"
7
+ hx-get="{% url 'plugins:netbox_atlassian: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 Atlassian data...</p>
16
+ </div>
17
+ </div>
18
+ </div>
19
+ {% endif %}
20
+ {% endblock %}
@@ -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 %}
@@ -1,7 +1,5 @@
1
- {% extends 'dcim/device/base.html' %}
2
1
  {% load helpers %}
3
2
 
4
- {% block content %}
5
3
  <div class="row">
6
4
  <div class="col-12">
7
5
  <!-- Search Terms Info -->
@@ -175,4 +173,3 @@
175
173
  </div>
176
174
  </div>
177
175
  </div>
178
- {% endblock %}
@@ -0,0 +1,20 @@
1
+ {% extends 'virtualization/virtualmachine/base.html' %}
2
+ {% load helpers %}
3
+
4
+ {% block content %}
5
+ {% if loading %}
6
+ <div id="atlassian-content"
7
+ hx-get="{% url 'plugins:netbox_atlassian:vm_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
+ </div>
17
+ </div>
18
+ </div>
19
+ {% endif %}
20
+ {% endblock %}
@@ -0,0 +1,30 @@
1
+ """
2
+ URL patterns for NetBox Atlassian Plugin
3
+ """
4
+
5
+ from django.urls import path
6
+
7
+ from .views import (
8
+ ENDPOINTS_PLUGIN_INSTALLED,
9
+ AtlassianSettingsView,
10
+ DeviceAtlassianContentView,
11
+ TestConfluenceConnectionView,
12
+ TestJiraConnectionView,
13
+ VMAtlassianContentView,
14
+ )
15
+
16
+ urlpatterns = [
17
+ path("settings/", AtlassianSettingsView.as_view(), name="settings"),
18
+ path("test-jira/", TestJiraConnectionView.as_view(), name="test_jira"),
19
+ path("test-confluence/", TestConfluenceConnectionView.as_view(), name="test_confluence"),
20
+ path("device/<int:pk>/content/", DeviceAtlassianContentView.as_view(), name="device_content"),
21
+ path("vm/<int:pk>/content/", VMAtlassianContentView.as_view(), name="vm_content"),
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
+ )
@@ -10,8 +10,10 @@ import re
10
10
  from dcim.models import Device
11
11
  from django.conf import settings
12
12
  from django.contrib import messages
13
- from django.http import JsonResponse
13
+ from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
14
+ from django.http import HttpResponse, JsonResponse
14
15
  from django.shortcuts import render
16
+ from django.template.loader import render_to_string
15
17
  from django.views import View
16
18
  from netbox.views import generic
17
19
  from utilities.views import ViewTab, register_model_view
@@ -20,6 +22,14 @@ from virtualization.models import VirtualMachine
20
22
  from .atlassian_client import get_client
21
23
  from .forms import AtlassianSettingsForm
22
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
+
23
33
 
24
34
  def get_device_attribute(device, attribute_path: str):
25
35
  """
@@ -51,6 +61,7 @@ def get_search_terms(device) -> list[str]:
51
61
  Get search terms from device based on configured search fields.
52
62
 
53
63
  Returns list of non-empty values from enabled search fields.
64
+ For comma-separated values (like multiple serial numbers), splits into individual terms.
54
65
  """
55
66
  config = settings.PLUGINS_CONFIG.get("netbox_atlassian", {})
56
67
  search_fields = config.get("search_fields", [])
@@ -66,7 +77,15 @@ def get_search_terms(device) -> list[str]:
66
77
 
67
78
  value = get_device_attribute(device, attribute)
68
79
  if value and value.strip():
69
- terms.append(value.strip())
80
+ # Split comma-separated values (e.g., multiple serial numbers)
81
+ if "," in value:
82
+ for part in value.split(","):
83
+ part = part.strip()
84
+ if part and part not in terms:
85
+ terms.append(part)
86
+ else:
87
+ if value.strip() not in terms:
88
+ terms.append(value.strip())
70
89
 
71
90
  return terms
72
91
 
@@ -108,7 +127,7 @@ def should_show_atlassian_tab(device) -> bool:
108
127
 
109
128
  @register_model_view(Device, name="atlassian", path="atlassian")
110
129
  class DeviceAtlassianView(generic.ObjectView):
111
- """Display Jira issues and Confluence pages for a Device."""
130
+ """Display Jira issues and Confluence pages for a Device with async loading."""
112
131
 
113
132
  queryset = Device.objects.all()
114
133
  template_name = "netbox_atlassian/device_tab.html"
@@ -121,7 +140,26 @@ class DeviceAtlassianView(generic.ObjectView):
121
140
  )
122
141
 
123
142
  def get(self, request, pk):
124
- """Handle GET request for the Atlassian tab."""
143
+ """Render initial tab with loading spinner - content loads via htmx."""
144
+ device = Device.objects.get(pk=pk)
145
+ return render(
146
+ request,
147
+ self.template_name,
148
+ {
149
+ "object": device,
150
+ "tab": self.tab,
151
+ "loading": True,
152
+ },
153
+ )
154
+
155
+
156
+ class DeviceAtlassianContentView(LoginRequiredMixin, PermissionRequiredMixin, View):
157
+ """HTMX endpoint that returns Atlassian content for async loading."""
158
+
159
+ permission_required = "dcim.view_device"
160
+
161
+ def get(self, request, pk):
162
+ """Fetch Atlassian data and return HTML content."""
125
163
  device = Device.objects.get(pk=pk)
126
164
 
127
165
  config = settings.PLUGINS_CONFIG.get("netbox_atlassian", {})
@@ -149,27 +187,28 @@ class DeviceAtlassianView(generic.ObjectView):
149
187
  jira_url = config.get("jira_url", "").rstrip("/")
150
188
  confluence_url = config.get("confluence_url", "").rstrip("/")
151
189
 
152
- return render(
153
- request,
154
- self.template_name,
155
- {
156
- "object": device,
157
- "tab": self.tab,
158
- "search_terms": search_terms,
159
- "enabled_fields": enabled_fields,
160
- "jira_results": jira_results,
161
- "confluence_results": confluence_results,
162
- "jira_url": jira_url,
163
- "confluence_url": confluence_url,
164
- "jira_configured": bool(jira_url),
165
- "confluence_configured": bool(confluence_url),
166
- },
190
+ return HttpResponse(
191
+ render_to_string(
192
+ "netbox_atlassian/tab_content.html",
193
+ {
194
+ "object": device,
195
+ "search_terms": search_terms,
196
+ "enabled_fields": enabled_fields,
197
+ "jira_results": jira_results,
198
+ "confluence_results": confluence_results,
199
+ "jira_url": jira_url,
200
+ "confluence_url": confluence_url,
201
+ "jira_configured": bool(jira_url),
202
+ "confluence_configured": bool(confluence_url),
203
+ },
204
+ request=request,
205
+ )
167
206
  )
168
207
 
169
208
 
170
209
  @register_model_view(VirtualMachine, name="atlassian", path="atlassian")
171
210
  class VirtualMachineAtlassianView(generic.ObjectView):
172
- """Display Jira issues and Confluence pages for a VirtualMachine."""
211
+ """Display Jira issues and Confluence pages for a VirtualMachine with async loading."""
173
212
 
174
213
  queryset = VirtualMachine.objects.all()
175
214
  template_name = "netbox_atlassian/vm_tab.html"
@@ -182,7 +221,26 @@ class VirtualMachineAtlassianView(generic.ObjectView):
182
221
  )
183
222
 
184
223
  def get(self, request, pk):
185
- """Handle GET request for the Atlassian tab."""
224
+ """Render initial tab with loading spinner - content loads via htmx."""
225
+ vm = VirtualMachine.objects.get(pk=pk)
226
+ return render(
227
+ request,
228
+ self.template_name,
229
+ {
230
+ "object": vm,
231
+ "tab": self.tab,
232
+ "loading": True,
233
+ },
234
+ )
235
+
236
+
237
+ class VMAtlassianContentView(LoginRequiredMixin, PermissionRequiredMixin, View):
238
+ """HTMX endpoint that returns Atlassian content for VM async loading."""
239
+
240
+ permission_required = "virtualization.view_virtualmachine"
241
+
242
+ def get(self, request, pk):
243
+ """Fetch Atlassian data and return HTML content."""
186
244
  vm = VirtualMachine.objects.get(pk=pk)
187
245
 
188
246
  config = settings.PLUGINS_CONFIG.get("netbox_atlassian", {})
@@ -196,7 +254,6 @@ class VirtualMachineAtlassianView(generic.ObjectView):
196
254
  search_terms.append(str(vm.primary_ip4.address.ip))
197
255
 
198
256
  # Get configured search fields for display
199
- search_fields = config.get("search_fields", [])
200
257
  enabled_fields = [{"name": "Name", "attribute": "name", "enabled": True}]
201
258
  if vm.primary_ip4:
202
259
  enabled_fields.append({"name": "Primary IP", "attribute": "primary_ip4", "enabled": True})
@@ -216,21 +273,22 @@ class VirtualMachineAtlassianView(generic.ObjectView):
216
273
  jira_url = config.get("jira_url", "").rstrip("/")
217
274
  confluence_url = config.get("confluence_url", "").rstrip("/")
218
275
 
219
- return render(
220
- request,
221
- self.template_name,
222
- {
223
- "object": vm,
224
- "tab": self.tab,
225
- "search_terms": search_terms,
226
- "enabled_fields": enabled_fields,
227
- "jira_results": jira_results,
228
- "confluence_results": confluence_results,
229
- "jira_url": jira_url,
230
- "confluence_url": confluence_url,
231
- "jira_configured": bool(jira_url),
232
- "confluence_configured": bool(confluence_url),
233
- },
276
+ return HttpResponse(
277
+ render_to_string(
278
+ "netbox_atlassian/tab_content.html",
279
+ {
280
+ "object": vm,
281
+ "search_terms": search_terms,
282
+ "enabled_fields": enabled_fields,
283
+ "jira_results": jira_results,
284
+ "confluence_results": confluence_results,
285
+ "jira_url": jira_url,
286
+ "confluence_url": confluence_url,
287
+ "jira_configured": bool(jira_url),
288
+ "confluence_configured": bool(confluence_url),
289
+ },
290
+ request=request,
291
+ )
234
292
  )
235
293
 
236
294
 
@@ -304,3 +362,145 @@ class TestConfluenceConnectionView(View):
304
362
  if success:
305
363
  return JsonResponse({"success": True, "message": message})
306
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.1
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
@@ -201,6 +201,12 @@ flake8 netbox_atlassian/
201
201
 
202
202
  See [CHANGELOG.md](CHANGELOG.md) for release history.
203
203
 
204
+ ## Support
205
+
206
+ If you find this plugin helpful, consider supporting development:
207
+
208
+ [![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)
209
+
204
210
  ## License
205
211
 
206
212
  Apache 2.0
@@ -12,5 +12,7 @@ netbox_atlassian.egg-info/dependency_links.txt
12
12
  netbox_atlassian.egg-info/requires.txt
13
13
  netbox_atlassian.egg-info/top_level.txt
14
14
  netbox_atlassian/templates/netbox_atlassian/device_tab.html
15
+ netbox_atlassian/templates/netbox_atlassian/endpoint_tab.html
15
16
  netbox_atlassian/templates/netbox_atlassian/settings.html
17
+ netbox_atlassian/templates/netbox_atlassian/tab_content.html
16
18
  netbox_atlassian/templates/netbox_atlassian/vm_tab.html
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "netbox-atlassian"
7
- version = "0.2.1"
7
+ version = "0.2.4"
8
8
  description = "NetBox plugin to display Jira issues and Confluence pages related to devices"
9
9
  readme = "README.md"
10
10
  license = {text = "Apache-2.0"}
@@ -1,74 +0,0 @@
1
- """
2
- NetBox Atlassian Plugin
3
-
4
- Display Jira issues and Confluence pages related to devices in NetBox.
5
- Searches by configurable fields (hostname, serial, role, etc.) with OR logic.
6
- """
7
-
8
- from netbox.plugins import PluginConfig
9
-
10
- __version__ = "0.2.1"
11
-
12
-
13
- class AtlassianConfig(PluginConfig):
14
- """Plugin configuration for NetBox Atlassian integration."""
15
-
16
- name = "netbox_atlassian"
17
- verbose_name = "Atlassian"
18
- description = "Display Jira issues and Confluence pages related to devices"
19
- version = __version__
20
- author = "sieteunoseis"
21
- author_email = "sieteunoseis@github.com"
22
- base_url = "atlassian"
23
- min_version = "4.0.0"
24
-
25
- # Required settings - plugin won't load without these
26
- required_settings = []
27
-
28
- # Default configuration values
29
- default_settings = {
30
- # Jira settings (on-prem)
31
- "jira_url": "", # e.g., "https://jira.example.com"
32
- "jira_username": "",
33
- "jira_password": "", # or API token
34
- "jira_token": "", # Personal Access Token (PAT) - preferred for on-prem
35
- "jira_verify_ssl": True,
36
- # Confluence settings (on-prem)
37
- "confluence_url": "", # e.g., "https://confluence.example.com"
38
- "confluence_username": "",
39
- "confluence_password": "", # or API token
40
- "confluence_token": "", # Personal Access Token (PAT) - preferred for on-prem
41
- "confluence_verify_ssl": True,
42
- # Cloud settings (for future use)
43
- "use_cloud": False,
44
- "cloud_api_token": "",
45
- "cloud_email": "",
46
- # Search configuration
47
- # Fields to search - values are device attribute paths
48
- # Searches use OR logic - matches any field
49
- "search_fields": [
50
- {"name": "Hostname", "attribute": "name", "enabled": True},
51
- {"name": "Serial", "attribute": "serial", "enabled": True},
52
- {"name": "Asset Tag", "attribute": "asset_tag", "enabled": False},
53
- {"name": "Role", "attribute": "role.name", "enabled": False},
54
- {"name": "Primary IP", "attribute": "primary_ip4.address", "enabled": False},
55
- ],
56
- # Jira search settings
57
- "jira_max_results": 10,
58
- "jira_projects": [], # Empty = search all projects
59
- "jira_issue_types": [], # Empty = all types
60
- # Confluence search settings
61
- "confluence_max_results": 10,
62
- "confluence_spaces": [], # Empty = search all spaces
63
- # General settings
64
- "timeout": 30,
65
- "cache_timeout": 300, # Cache results for 5 minutes
66
- # Enable legacy SSL renegotiation for older servers (required for OHSU wiki)
67
- "enable_legacy_ssl": False,
68
- # Device type filtering (like catalyst-center)
69
- # Empty list = show tab for all devices
70
- "device_types": [], # e.g., ["cisco", "juniper"]
71
- }
72
-
73
-
74
- config = AtlassianConfig
@@ -1,178 +0,0 @@
1
- {% extends 'virtualization/virtualmachine/base.html' %}
2
- {% load helpers %}
3
-
4
- {% block content %}
5
- <div class="row">
6
- <div class="col-12">
7
- <!-- Search Terms Info -->
8
- <div class="alert alert-info mb-3">
9
- <i class="mdi mdi-magnify"></i>
10
- <strong>Searching for:</strong>
11
- {% for term in search_terms %}
12
- <span class="badge bg-info text-white ms-1">{{ term }}</span>{% if not forloop.last %}<span class="text-muted ms-1">OR</span>{% endif %}
13
- {% empty %}
14
- <span class="text-muted">No search terms available</span>
15
- {% endfor %}
16
- <br>
17
- <small class="text-muted">
18
- Fields: {% for field in enabled_fields %}{{ field.name }}{% if not forloop.last %}, {% endif %}{% endfor %}
19
- </small>
20
- </div>
21
-
22
- <div class="row">
23
- <!-- Jira Issues Column -->
24
- <div class="col-md-6 mb-4">
25
- <div class="card h-100">
26
- <div class="card-header d-flex justify-content-between align-items-center">
27
- <h5 class="card-title mb-0">
28
- <i class="mdi mdi-jira text-primary"></i> Jira Issues
29
- {% if jira_results.total > 0 %}
30
- <span class="badge bg-info text-white">{{ jira_results.total }} issue{{ jira_results.total|pluralize }}</span>
31
- {% endif %}
32
- </h5>
33
- {% if jira_url %}
34
- <a href="{{ jira_url }}" target="_blank" class="btn btn-sm btn-outline-primary">
35
- <i class="mdi mdi-open-in-new"></i> Open Jira
36
- </a>
37
- {% endif %}
38
- </div>
39
- <div class="card-body">
40
- {% if not jira_configured %}
41
- <div class="alert alert-warning mb-0">
42
- <i class="mdi mdi-alert"></i> Jira not configured
43
- </div>
44
- {% elif jira_results.error %}
45
- <div class="alert alert-danger mb-0">
46
- <i class="mdi mdi-alert-circle"></i> {{ jira_results.error }}
47
- </div>
48
- {% elif jira_results.issues %}
49
- <div class="table-responsive">
50
- <table class="table table-sm table-hover mb-0">
51
- <thead>
52
- <tr>
53
- <th>Key</th>
54
- <th>Summary</th>
55
- <th>Status</th>
56
- <th>Type</th>
57
- </tr>
58
- </thead>
59
- <tbody>
60
- {% for issue in jira_results.issues %}
61
- <tr>
62
- <td>
63
- <a href="{{ issue.url }}" target="_blank" class="fw-bold">
64
- {{ issue.key }}
65
- </a>
66
- </td>
67
- <td>
68
- <span title="{{ issue.summary }}">
69
- {{ issue.summary|truncatechars:50 }}
70
- </span>
71
- </td>
72
- <td>
73
- {% if issue.status_category == 'done' %}
74
- <span class="badge bg-success text-white">{{ issue.status }}</span>
75
- {% elif issue.status_category == 'indeterminate' %}
76
- <span class="badge bg-primary text-white">{{ issue.status }}</span>
77
- {% else %}
78
- <span class="badge bg-secondary text-white">{{ issue.status }}</span>
79
- {% endif %}
80
- </td>
81
- <td>
82
- {% if issue.type_icon %}
83
- <img src="{{ issue.type_icon }}" alt="{{ issue.type }}" width="16" height="16" class="me-1">
84
- {% endif %}
85
- {{ issue.type }}
86
- </td>
87
- </tr>
88
- {% endfor %}
89
- </tbody>
90
- </table>
91
- </div>
92
- {% if jira_results.total > jira_results.issues|length %}
93
- <div class="text-muted small mt-2">
94
- Showing {{ jira_results.issues|length }} of {{ jira_results.total }} results
95
- </div>
96
- {% endif %}
97
- {% else %}
98
- <div class="text-muted text-center py-4">
99
- <i class="mdi mdi-information-outline fs-1"></i>
100
- <p class="mb-0">No Jira issues found</p>
101
- </div>
102
- {% endif %}
103
-
104
- {% if jira_results.cached %}
105
- <div class="text-muted small mt-2">
106
- <i class="mdi mdi-cached"></i> Results from cache
107
- </div>
108
- {% endif %}
109
- </div>
110
- </div>
111
- </div>
112
-
113
- <!-- Confluence Pages Column -->
114
- <div class="col-md-6 mb-4">
115
- <div class="card h-100">
116
- <div class="card-header d-flex justify-content-between align-items-center">
117
- <h5 class="card-title mb-0">
118
- <i class="mdi mdi-file-document text-info"></i> Confluence Pages
119
- {% if confluence_results.total > 0 %}
120
- <span class="badge bg-info text-white">{{ confluence_results.total }} page{{ confluence_results.total|pluralize }}</span>
121
- {% endif %}
122
- </h5>
123
- {% if confluence_url %}
124
- <a href="{{ confluence_url }}" target="_blank" class="btn btn-sm btn-outline-info">
125
- <i class="mdi mdi-open-in-new"></i> Open Confluence
126
- </a>
127
- {% endif %}
128
- </div>
129
- <div class="card-body">
130
- {% if not confluence_configured %}
131
- <div class="alert alert-warning mb-0">
132
- <i class="mdi mdi-alert"></i> Confluence not configured
133
- </div>
134
- {% elif confluence_results.error %}
135
- <div class="alert alert-danger mb-0">
136
- <i class="mdi mdi-alert-circle"></i> {{ confluence_results.error }}
137
- </div>
138
- {% elif confluence_results.pages %}
139
- <div class="list-group list-group-flush">
140
- {% for page in confluence_results.pages %}
141
- <a href="{{ page.url }}" target="_blank" class="list-group-item list-group-item-action">
142
- <div class="d-flex w-100 justify-content-between">
143
- <h6 class="mb-1">{{ page.title }}</h6>
144
- <small class="text-muted">{{ page.space_key }}</small>
145
- </div>
146
- {% if page.breadcrumb %}
147
- <small class="text-muted">{{ page.breadcrumb }}</small>
148
- {% endif %}
149
- <small class="text-muted d-block">
150
- Last modified by {{ page.last_modified_by }}
151
- </small>
152
- </a>
153
- {% endfor %}
154
- </div>
155
- {% if confluence_results.total > confluence_results.pages|length %}
156
- <div class="text-muted small mt-2">
157
- Showing {{ confluence_results.pages|length }} of {{ confluence_results.total }} results
158
- </div>
159
- {% endif %}
160
- {% else %}
161
- <div class="text-muted text-center py-4">
162
- <i class="mdi mdi-information-outline fs-1"></i>
163
- <p class="mb-0">No Confluence pages found</p>
164
- </div>
165
- {% endif %}
166
-
167
- {% if confluence_results.cached %}
168
- <div class="text-muted small mt-2">
169
- <i class="mdi mdi-cached"></i> Results from cache
170
- </div>
171
- {% endif %}
172
- </div>
173
- </div>
174
- </div>
175
- </div>
176
- </div>
177
- </div>
178
- {% endblock %}
@@ -1,17 +0,0 @@
1
- """
2
- URL patterns for NetBox Atlassian Plugin
3
- """
4
-
5
- from django.urls import path
6
-
7
- from .views import (
8
- AtlassianSettingsView,
9
- TestConfluenceConnectionView,
10
- TestJiraConnectionView,
11
- )
12
-
13
- urlpatterns = [
14
- path("settings/", AtlassianSettingsView.as_view(), name="settings"),
15
- path("test-jira/", TestJiraConnectionView.as_view(), name="test_jira"),
16
- path("test-confluence/", TestConfluenceConnectionView.as_view(), name="test_confluence"),
17
- ]