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.
- {netbox_atlassian-0.2.1 → netbox_atlassian-0.2.4}/PKG-INFO +7 -1
- {netbox_atlassian-0.2.1 → netbox_atlassian-0.2.4}/README.md +6 -0
- netbox_atlassian-0.2.4/netbox_atlassian/__init__.py +148 -0
- {netbox_atlassian-0.2.1 → netbox_atlassian-0.2.4}/netbox_atlassian/atlassian_client.py +32 -26
- netbox_atlassian-0.2.4/netbox_atlassian/templates/netbox_atlassian/device_tab.html +20 -0
- netbox_atlassian-0.2.4/netbox_atlassian/templates/netbox_atlassian/endpoint_tab.html +25 -0
- 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
- netbox_atlassian-0.2.4/netbox_atlassian/templates/netbox_atlassian/vm_tab.html +20 -0
- netbox_atlassian-0.2.4/netbox_atlassian/urls.py +30 -0
- {netbox_atlassian-0.2.1 → netbox_atlassian-0.2.4}/netbox_atlassian/views.py +237 -37
- {netbox_atlassian-0.2.1 → netbox_atlassian-0.2.4}/netbox_atlassian.egg-info/PKG-INFO +7 -1
- {netbox_atlassian-0.2.1 → netbox_atlassian-0.2.4}/netbox_atlassian.egg-info/SOURCES.txt +2 -0
- {netbox_atlassian-0.2.1 → netbox_atlassian-0.2.4}/pyproject.toml +1 -1
- netbox_atlassian-0.2.1/netbox_atlassian/__init__.py +0 -74
- netbox_atlassian-0.2.1/netbox_atlassian/templates/netbox_atlassian/vm_tab.html +0 -178
- netbox_atlassian-0.2.1/netbox_atlassian/urls.py +0 -17
- {netbox_atlassian-0.2.1 → netbox_atlassian-0.2.4}/netbox_atlassian/forms.py +0 -0
- {netbox_atlassian-0.2.1 → netbox_atlassian-0.2.4}/netbox_atlassian/navigation.py +0 -0
- {netbox_atlassian-0.2.1 → netbox_atlassian-0.2.4}/netbox_atlassian/templates/netbox_atlassian/settings.html +0 -0
- {netbox_atlassian-0.2.1 → netbox_atlassian-0.2.4}/netbox_atlassian.egg-info/dependency_links.txt +0 -0
- {netbox_atlassian-0.2.1 → netbox_atlassian-0.2.4}/netbox_atlassian.egg-info/requires.txt +0 -0
- {netbox_atlassian-0.2.1 → netbox_atlassian-0.2.4}/netbox_atlassian.egg-info/top_level.txt +0 -0
- {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.
|
|
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
|
+
[](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
|
+
[](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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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 %}
|
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
"""
|
|
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
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
"""
|
|
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
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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.
|
|
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
|
+
[](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.
|
|
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
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{netbox_atlassian-0.2.1 → netbox_atlassian-0.2.4}/netbox_atlassian.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|