netbox-atlassian 0.1.0__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.
- netbox_atlassian/__init__.py +73 -0
- netbox_atlassian/atlassian_client.py +346 -0
- netbox_atlassian/forms.py +101 -0
- netbox_atlassian/navigation.py +22 -0
- netbox_atlassian/templates/netbox_atlassian/device_tab.html +178 -0
- netbox_atlassian/templates/netbox_atlassian/settings.html +274 -0
- netbox_atlassian/templates/netbox_atlassian/vm_tab.html +178 -0
- netbox_atlassian/urls.py +17 -0
- netbox_atlassian/views.py +306 -0
- netbox_atlassian-0.1.0.dist-info/METADATA +186 -0
- netbox_atlassian-0.1.0.dist-info/RECORD +13 -0
- netbox_atlassian-0.1.0.dist-info/WHEEL +5 -0
- netbox_atlassian-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,73 @@
|
|
|
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.1.0"
|
|
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_verify_ssl": True,
|
|
35
|
+
# Confluence settings (on-prem)
|
|
36
|
+
"confluence_url": "", # e.g., "https://confluence.example.com"
|
|
37
|
+
"confluence_username": "",
|
|
38
|
+
"confluence_password": "", # or API token
|
|
39
|
+
"confluence_token": "", # Personal Access Token (PAT) - preferred for on-prem
|
|
40
|
+
"confluence_verify_ssl": True,
|
|
41
|
+
# Cloud settings (for future use)
|
|
42
|
+
"use_cloud": False,
|
|
43
|
+
"cloud_api_token": "",
|
|
44
|
+
"cloud_email": "",
|
|
45
|
+
# Search configuration
|
|
46
|
+
# Fields to search - values are device attribute paths
|
|
47
|
+
# Searches use OR logic - matches any field
|
|
48
|
+
"search_fields": [
|
|
49
|
+
{"name": "Hostname", "attribute": "name", "enabled": True},
|
|
50
|
+
{"name": "Serial", "attribute": "serial", "enabled": True},
|
|
51
|
+
{"name": "Asset Tag", "attribute": "asset_tag", "enabled": False},
|
|
52
|
+
{"name": "Role", "attribute": "role.name", "enabled": False},
|
|
53
|
+
{"name": "Primary IP", "attribute": "primary_ip4.address", "enabled": False},
|
|
54
|
+
],
|
|
55
|
+
# Jira search settings
|
|
56
|
+
"jira_max_results": 10,
|
|
57
|
+
"jira_projects": [], # Empty = search all projects
|
|
58
|
+
"jira_issue_types": [], # Empty = all types
|
|
59
|
+
# Confluence search settings
|
|
60
|
+
"confluence_max_results": 10,
|
|
61
|
+
"confluence_spaces": [], # Empty = search all spaces
|
|
62
|
+
# General settings
|
|
63
|
+
"timeout": 30,
|
|
64
|
+
"cache_timeout": 300, # Cache results for 5 minutes
|
|
65
|
+
# Enable legacy SSL renegotiation for older servers (required for OHSU wiki)
|
|
66
|
+
"enable_legacy_ssl": False,
|
|
67
|
+
# Device type filtering (like catalyst-center)
|
|
68
|
+
# Empty list = show tab for all devices
|
|
69
|
+
"device_types": [], # e.g., ["cisco", "juniper"]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
config = AtlassianConfig
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Atlassian API Client for Jira and Confluence
|
|
3
|
+
|
|
4
|
+
Supports both on-premise and cloud deployments.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import ssl
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
import requests
|
|
12
|
+
from django.conf import settings
|
|
13
|
+
from django.core.cache import cache
|
|
14
|
+
from requests.adapters import HTTPAdapter
|
|
15
|
+
from urllib3.util.ssl_ import create_urllib3_context
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class LegacySSLAdapter(HTTPAdapter):
|
|
21
|
+
"""
|
|
22
|
+
HTTP Adapter that enables legacy SSL renegotiation.
|
|
23
|
+
|
|
24
|
+
Required for older servers (like OHSU Confluence) that don't support
|
|
25
|
+
secure renegotiation.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def init_poolmanager(self, *args, **kwargs):
|
|
29
|
+
ctx = create_urllib3_context()
|
|
30
|
+
# Enable legacy renegotiation for older servers
|
|
31
|
+
ctx.options |= ssl.OP_LEGACY_SERVER_CONNECT
|
|
32
|
+
kwargs["ssl_context"] = ctx
|
|
33
|
+
return super().init_poolmanager(*args, **kwargs)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AtlassianClient:
|
|
37
|
+
"""Client for Jira and Confluence REST APIs."""
|
|
38
|
+
|
|
39
|
+
def __init__(self):
|
|
40
|
+
"""Initialize the Atlassian client from plugin settings."""
|
|
41
|
+
self.config = settings.PLUGINS_CONFIG.get("netbox_atlassian", {})
|
|
42
|
+
|
|
43
|
+
# Jira settings
|
|
44
|
+
self.jira_url = self.config.get("jira_url", "").rstrip("/")
|
|
45
|
+
self.jira_username = self.config.get("jira_username", "")
|
|
46
|
+
self.jira_password = self.config.get("jira_password", "")
|
|
47
|
+
self.jira_verify_ssl = self.config.get("jira_verify_ssl", True)
|
|
48
|
+
|
|
49
|
+
# Confluence settings
|
|
50
|
+
self.confluence_url = self.config.get("confluence_url", "").rstrip("/")
|
|
51
|
+
self.confluence_username = self.config.get("confluence_username", "")
|
|
52
|
+
self.confluence_password = self.config.get("confluence_password", "")
|
|
53
|
+
self.confluence_token = self.config.get("confluence_token", "") # Personal Access Token
|
|
54
|
+
self.confluence_verify_ssl = self.config.get("confluence_verify_ssl", True)
|
|
55
|
+
|
|
56
|
+
# Cloud settings
|
|
57
|
+
self.use_cloud = self.config.get("use_cloud", False)
|
|
58
|
+
self.cloud_api_token = self.config.get("cloud_api_token", "")
|
|
59
|
+
self.cloud_email = self.config.get("cloud_email", "")
|
|
60
|
+
|
|
61
|
+
# General settings
|
|
62
|
+
self.timeout = self.config.get("timeout", 30)
|
|
63
|
+
self.cache_timeout = self.config.get("cache_timeout", 300)
|
|
64
|
+
|
|
65
|
+
# Legacy SSL support (for servers requiring legacy renegotiation)
|
|
66
|
+
self.enable_legacy_ssl = self.config.get("enable_legacy_ssl", False)
|
|
67
|
+
|
|
68
|
+
# Create session with legacy SSL if needed
|
|
69
|
+
self._session = None
|
|
70
|
+
|
|
71
|
+
def _get_session(self) -> requests.Session:
|
|
72
|
+
"""Get or create a requests session with optional legacy SSL support."""
|
|
73
|
+
if self._session is None:
|
|
74
|
+
self._session = requests.Session()
|
|
75
|
+
if self.enable_legacy_ssl:
|
|
76
|
+
adapter = LegacySSLAdapter()
|
|
77
|
+
self._session.mount("https://", adapter)
|
|
78
|
+
return self._session
|
|
79
|
+
|
|
80
|
+
def _get_jira_auth(self):
|
|
81
|
+
"""Get authentication for Jira API."""
|
|
82
|
+
if self.use_cloud:
|
|
83
|
+
return (self.cloud_email, self.cloud_api_token)
|
|
84
|
+
return (self.jira_username, self.jira_password)
|
|
85
|
+
|
|
86
|
+
def _get_confluence_auth(self):
|
|
87
|
+
"""Get authentication for Confluence API."""
|
|
88
|
+
if self.use_cloud:
|
|
89
|
+
return (self.cloud_email, self.cloud_api_token)
|
|
90
|
+
return (self.confluence_username, self.confluence_password)
|
|
91
|
+
|
|
92
|
+
def _jira_request(self, endpoint: str, params: Optional[dict] = None) -> Optional[dict]:
|
|
93
|
+
"""Make a request to Jira REST API."""
|
|
94
|
+
if not self.jira_url:
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
url = f"{self.jira_url}/rest/api/2/{endpoint}"
|
|
98
|
+
session = self._get_session()
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
response = session.get(
|
|
102
|
+
url,
|
|
103
|
+
auth=self._get_jira_auth(),
|
|
104
|
+
params=params,
|
|
105
|
+
verify=self.jira_verify_ssl,
|
|
106
|
+
timeout=self.timeout,
|
|
107
|
+
)
|
|
108
|
+
response.raise_for_status()
|
|
109
|
+
return response.json()
|
|
110
|
+
except requests.exceptions.RequestException as e:
|
|
111
|
+
logger.error(f"Jira API error: {e}")
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
def _confluence_request(self, endpoint: str, params: Optional[dict] = None) -> Optional[dict]:
|
|
115
|
+
"""Make a request to Confluence REST API."""
|
|
116
|
+
if not self.confluence_url:
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
url = f"{self.confluence_url}/rest/api/{endpoint}"
|
|
120
|
+
session = self._get_session()
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
# Use PAT (Bearer token) if available, otherwise basic auth
|
|
124
|
+
if self.confluence_token:
|
|
125
|
+
headers = {"Authorization": f"Bearer {self.confluence_token}"}
|
|
126
|
+
response = session.get(
|
|
127
|
+
url,
|
|
128
|
+
headers=headers,
|
|
129
|
+
params=params,
|
|
130
|
+
verify=self.confluence_verify_ssl,
|
|
131
|
+
timeout=self.timeout,
|
|
132
|
+
)
|
|
133
|
+
else:
|
|
134
|
+
response = session.get(
|
|
135
|
+
url,
|
|
136
|
+
auth=self._get_confluence_auth(),
|
|
137
|
+
params=params,
|
|
138
|
+
verify=self.confluence_verify_ssl,
|
|
139
|
+
timeout=self.timeout,
|
|
140
|
+
)
|
|
141
|
+
response.raise_for_status()
|
|
142
|
+
return response.json()
|
|
143
|
+
except requests.exceptions.RequestException as e:
|
|
144
|
+
logger.error(f"Confluence API error: {e}")
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
def search_jira(self, search_terms: list[str], max_results: int = 10) -> dict:
|
|
148
|
+
"""
|
|
149
|
+
Search Jira for issues containing any of the search terms.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
search_terms: List of terms to search (OR logic)
|
|
153
|
+
max_results: Maximum number of results to return
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
dict with 'issues' list and 'total' count
|
|
157
|
+
"""
|
|
158
|
+
if not self.jira_url or not search_terms:
|
|
159
|
+
return {"issues": [], "total": 0, "error": None}
|
|
160
|
+
|
|
161
|
+
# Build JQL query with OR logic
|
|
162
|
+
# Search in summary, description, and comments
|
|
163
|
+
text_queries = []
|
|
164
|
+
for term in search_terms:
|
|
165
|
+
if term:
|
|
166
|
+
# Escape special JQL characters
|
|
167
|
+
escaped = term.replace('"', '\\"')
|
|
168
|
+
text_queries.append(f'text ~ "{escaped}"')
|
|
169
|
+
|
|
170
|
+
if not text_queries:
|
|
171
|
+
return {"issues": [], "total": 0, "error": None}
|
|
172
|
+
|
|
173
|
+
jql = " OR ".join(text_queries)
|
|
174
|
+
|
|
175
|
+
# Add project filter if configured
|
|
176
|
+
projects = self.config.get("jira_projects", [])
|
|
177
|
+
if projects:
|
|
178
|
+
project_jql = " OR ".join([f'project = "{p}"' for p in projects])
|
|
179
|
+
jql = f"({jql}) AND ({project_jql})"
|
|
180
|
+
|
|
181
|
+
# Add issue type filter if configured
|
|
182
|
+
issue_types = self.config.get("jira_issue_types", [])
|
|
183
|
+
if issue_types:
|
|
184
|
+
type_jql = " OR ".join([f'issuetype = "{t}"' for t in issue_types])
|
|
185
|
+
jql = f"({jql}) AND ({type_jql})"
|
|
186
|
+
|
|
187
|
+
# Order by updated date descending
|
|
188
|
+
jql += " ORDER BY updated DESC"
|
|
189
|
+
|
|
190
|
+
cache_key = f"atlassian_jira_{hash(jql)}"
|
|
191
|
+
cached = cache.get(cache_key)
|
|
192
|
+
if cached:
|
|
193
|
+
cached["cached"] = True
|
|
194
|
+
return cached
|
|
195
|
+
|
|
196
|
+
params = {
|
|
197
|
+
"jql": jql,
|
|
198
|
+
"maxResults": max_results,
|
|
199
|
+
"fields": "summary,status,issuetype,priority,assignee,created,updated,project",
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
result = self._jira_request("search", params)
|
|
203
|
+
|
|
204
|
+
if result is None:
|
|
205
|
+
return {"issues": [], "total": 0, "error": "Failed to connect to Jira"}
|
|
206
|
+
|
|
207
|
+
issues = []
|
|
208
|
+
for issue in result.get("issues", []):
|
|
209
|
+
fields = issue.get("fields", {})
|
|
210
|
+
issues.append({
|
|
211
|
+
"key": issue.get("key"),
|
|
212
|
+
"summary": fields.get("summary", ""),
|
|
213
|
+
"status": fields.get("status", {}).get("name", ""),
|
|
214
|
+
"status_category": fields.get("status", {}).get("statusCategory", {}).get("key", ""),
|
|
215
|
+
"type": fields.get("issuetype", {}).get("name", ""),
|
|
216
|
+
"type_icon": fields.get("issuetype", {}).get("iconUrl", ""),
|
|
217
|
+
"priority": fields.get("priority", {}).get("name", "") if fields.get("priority") else "",
|
|
218
|
+
"priority_icon": fields.get("priority", {}).get("iconUrl", "") if fields.get("priority") else "",
|
|
219
|
+
"assignee": fields.get("assignee", {}).get("displayName", "") if fields.get("assignee") else "Unassigned",
|
|
220
|
+
"created": fields.get("created", ""),
|
|
221
|
+
"updated": fields.get("updated", ""),
|
|
222
|
+
"project": fields.get("project", {}).get("name", ""),
|
|
223
|
+
"project_key": fields.get("project", {}).get("key", ""),
|
|
224
|
+
"url": f"{self.jira_url}/browse/{issue.get('key')}",
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
response = {
|
|
228
|
+
"issues": issues,
|
|
229
|
+
"total": result.get("total", 0),
|
|
230
|
+
"error": None,
|
|
231
|
+
"cached": False,
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
cache.set(cache_key, response, self.cache_timeout)
|
|
235
|
+
return response
|
|
236
|
+
|
|
237
|
+
def search_confluence(self, search_terms: list[str], max_results: int = 10) -> dict:
|
|
238
|
+
"""
|
|
239
|
+
Search Confluence for pages containing any of the search terms.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
search_terms: List of terms to search (OR logic)
|
|
243
|
+
max_results: Maximum number of results to return
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
dict with 'pages' list and 'total' count
|
|
247
|
+
"""
|
|
248
|
+
if not self.confluence_url or not search_terms:
|
|
249
|
+
return {"pages": [], "total": 0, "error": None}
|
|
250
|
+
|
|
251
|
+
# Build CQL query with OR logic
|
|
252
|
+
text_queries = []
|
|
253
|
+
for term in search_terms:
|
|
254
|
+
if term:
|
|
255
|
+
# Escape special CQL characters
|
|
256
|
+
escaped = term.replace('"', '\\"')
|
|
257
|
+
text_queries.append(f'text ~ "{escaped}"')
|
|
258
|
+
|
|
259
|
+
if not text_queries:
|
|
260
|
+
return {"pages": [], "total": 0, "error": None}
|
|
261
|
+
|
|
262
|
+
cql = " OR ".join(text_queries)
|
|
263
|
+
|
|
264
|
+
# Filter to pages only (not attachments, comments, etc.)
|
|
265
|
+
cql = f"({cql}) AND type = page"
|
|
266
|
+
|
|
267
|
+
# Add space filter if configured
|
|
268
|
+
spaces = self.config.get("confluence_spaces", [])
|
|
269
|
+
if spaces:
|
|
270
|
+
space_cql = " OR ".join([f'space = "{s}"' for s in spaces])
|
|
271
|
+
cql = f"({cql}) AND ({space_cql})"
|
|
272
|
+
|
|
273
|
+
cache_key = f"atlassian_confluence_{hash(cql)}"
|
|
274
|
+
cached = cache.get(cache_key)
|
|
275
|
+
if cached:
|
|
276
|
+
cached["cached"] = True
|
|
277
|
+
return cached
|
|
278
|
+
|
|
279
|
+
params = {
|
|
280
|
+
"cql": cql,
|
|
281
|
+
"limit": max_results,
|
|
282
|
+
"expand": "space,version,ancestors",
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
result = self._confluence_request("content/search", params)
|
|
286
|
+
|
|
287
|
+
if result is None:
|
|
288
|
+
return {"pages": [], "total": 0, "error": "Failed to connect to Confluence"}
|
|
289
|
+
|
|
290
|
+
pages = []
|
|
291
|
+
for page in result.get("results", []):
|
|
292
|
+
space = page.get("space", {})
|
|
293
|
+
version = page.get("version", {})
|
|
294
|
+
|
|
295
|
+
# Build breadcrumb from ancestors
|
|
296
|
+
ancestors = page.get("ancestors", [])
|
|
297
|
+
breadcrumb = " > ".join([a.get("title", "") for a in ancestors])
|
|
298
|
+
|
|
299
|
+
pages.append({
|
|
300
|
+
"id": page.get("id"),
|
|
301
|
+
"title": page.get("title", ""),
|
|
302
|
+
"space_key": space.get("key", ""),
|
|
303
|
+
"space_name": space.get("name", ""),
|
|
304
|
+
"last_modified": version.get("when", ""),
|
|
305
|
+
"last_modified_by": version.get("by", {}).get("displayName", ""),
|
|
306
|
+
"breadcrumb": breadcrumb,
|
|
307
|
+
"url": f"{self.confluence_url}{page.get('_links', {}).get('webui', '')}",
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
response = {
|
|
311
|
+
"pages": pages,
|
|
312
|
+
"total": result.get("totalSize", result.get("size", 0)),
|
|
313
|
+
"error": None,
|
|
314
|
+
"cached": False,
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
cache.set(cache_key, response, self.cache_timeout)
|
|
318
|
+
return response
|
|
319
|
+
|
|
320
|
+
def test_jira_connection(self) -> tuple[bool, str]:
|
|
321
|
+
"""Test Jira connection."""
|
|
322
|
+
if not self.jira_url:
|
|
323
|
+
return False, "Jira URL not configured"
|
|
324
|
+
|
|
325
|
+
result = self._jira_request("myself")
|
|
326
|
+
if result:
|
|
327
|
+
return True, f"Connected as {result.get('displayName', result.get('name', 'Unknown'))}"
|
|
328
|
+
return False, "Failed to connect to Jira"
|
|
329
|
+
|
|
330
|
+
def test_confluence_connection(self) -> tuple[bool, str]:
|
|
331
|
+
"""Test Confluence connection."""
|
|
332
|
+
if not self.confluence_url:
|
|
333
|
+
return False, "Confluence URL not configured"
|
|
334
|
+
|
|
335
|
+
if not self.confluence_token and not self.confluence_username:
|
|
336
|
+
return False, "Confluence credentials not configured (need token or username/password)"
|
|
337
|
+
|
|
338
|
+
result = self._confluence_request("user/current")
|
|
339
|
+
if result:
|
|
340
|
+
return True, f"Connected as {result.get('displayName', result.get('username', 'Unknown'))}"
|
|
341
|
+
return False, "Failed to connect to Confluence"
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def get_client() -> AtlassianClient:
|
|
345
|
+
"""Get a configured Atlassian client instance."""
|
|
346
|
+
return AtlassianClient()
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Forms for NetBox Atlassian Plugin
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from django import forms
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AtlassianSettingsForm(forms.Form):
|
|
9
|
+
"""Form for displaying and validating Atlassian settings."""
|
|
10
|
+
|
|
11
|
+
# Jira settings
|
|
12
|
+
jira_url = forms.URLField(
|
|
13
|
+
required=False,
|
|
14
|
+
label="Jira URL",
|
|
15
|
+
help_text="On-premise Jira server URL (e.g., https://jira.example.com)",
|
|
16
|
+
widget=forms.URLInput(attrs={"class": "form-control", "placeholder": "https://jira.example.com"}),
|
|
17
|
+
)
|
|
18
|
+
jira_username = forms.CharField(
|
|
19
|
+
required=False,
|
|
20
|
+
label="Jira Username",
|
|
21
|
+
help_text="Username for Jira authentication",
|
|
22
|
+
widget=forms.TextInput(attrs={"class": "form-control"}),
|
|
23
|
+
)
|
|
24
|
+
jira_password = forms.CharField(
|
|
25
|
+
required=False,
|
|
26
|
+
label="Jira Password/Token",
|
|
27
|
+
help_text="Password or API token for Jira authentication",
|
|
28
|
+
widget=forms.PasswordInput(attrs={"class": "form-control"}),
|
|
29
|
+
)
|
|
30
|
+
jira_verify_ssl = forms.BooleanField(
|
|
31
|
+
required=False,
|
|
32
|
+
initial=True,
|
|
33
|
+
label="Verify Jira SSL",
|
|
34
|
+
help_text="Verify SSL certificate for Jira connection",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Confluence settings
|
|
38
|
+
confluence_url = forms.URLField(
|
|
39
|
+
required=False,
|
|
40
|
+
label="Confluence URL",
|
|
41
|
+
help_text="On-premise Confluence server URL (e.g., https://confluence.example.com)",
|
|
42
|
+
widget=forms.URLInput(attrs={"class": "form-control", "placeholder": "https://confluence.example.com"}),
|
|
43
|
+
)
|
|
44
|
+
confluence_username = forms.CharField(
|
|
45
|
+
required=False,
|
|
46
|
+
label="Confluence Username",
|
|
47
|
+
help_text="Username for Confluence authentication",
|
|
48
|
+
widget=forms.TextInput(attrs={"class": "form-control"}),
|
|
49
|
+
)
|
|
50
|
+
confluence_password = forms.CharField(
|
|
51
|
+
required=False,
|
|
52
|
+
label="Confluence Password/Token",
|
|
53
|
+
help_text="Password or API token for Confluence authentication",
|
|
54
|
+
widget=forms.PasswordInput(attrs={"class": "form-control"}),
|
|
55
|
+
)
|
|
56
|
+
confluence_verify_ssl = forms.BooleanField(
|
|
57
|
+
required=False,
|
|
58
|
+
initial=True,
|
|
59
|
+
label="Verify Confluence SSL",
|
|
60
|
+
help_text="Verify SSL certificate for Confluence connection",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Search settings
|
|
64
|
+
jira_max_results = forms.IntegerField(
|
|
65
|
+
required=False,
|
|
66
|
+
initial=10,
|
|
67
|
+
min_value=1,
|
|
68
|
+
max_value=100,
|
|
69
|
+
label="Jira Max Results",
|
|
70
|
+
help_text="Maximum number of Jira issues to display",
|
|
71
|
+
widget=forms.NumberInput(attrs={"class": "form-control"}),
|
|
72
|
+
)
|
|
73
|
+
confluence_max_results = forms.IntegerField(
|
|
74
|
+
required=False,
|
|
75
|
+
initial=10,
|
|
76
|
+
min_value=1,
|
|
77
|
+
max_value=100,
|
|
78
|
+
label="Confluence Max Results",
|
|
79
|
+
help_text="Maximum number of Confluence pages to display",
|
|
80
|
+
widget=forms.NumberInput(attrs={"class": "form-control"}),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# General settings
|
|
84
|
+
timeout = forms.IntegerField(
|
|
85
|
+
required=False,
|
|
86
|
+
initial=30,
|
|
87
|
+
min_value=5,
|
|
88
|
+
max_value=120,
|
|
89
|
+
label="Timeout",
|
|
90
|
+
help_text="API timeout in seconds",
|
|
91
|
+
widget=forms.NumberInput(attrs={"class": "form-control"}),
|
|
92
|
+
)
|
|
93
|
+
cache_timeout = forms.IntegerField(
|
|
94
|
+
required=False,
|
|
95
|
+
initial=300,
|
|
96
|
+
min_value=0,
|
|
97
|
+
max_value=3600,
|
|
98
|
+
label="Cache Timeout",
|
|
99
|
+
help_text="Cache timeout in seconds (0 to disable caching)",
|
|
100
|
+
widget=forms.NumberInput(attrs={"class": "form-control"}),
|
|
101
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Navigation menu items for NetBox Atlassian Plugin
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from netbox.plugins import PluginMenu, PluginMenuItem
|
|
6
|
+
|
|
7
|
+
menu = PluginMenu(
|
|
8
|
+
label="Atlassian",
|
|
9
|
+
groups=(
|
|
10
|
+
(
|
|
11
|
+
"Settings",
|
|
12
|
+
(
|
|
13
|
+
PluginMenuItem(
|
|
14
|
+
link="plugins:netbox_atlassian:settings",
|
|
15
|
+
link_text="Configuration",
|
|
16
|
+
permissions=["dcim.view_device"],
|
|
17
|
+
),
|
|
18
|
+
),
|
|
19
|
+
),
|
|
20
|
+
),
|
|
21
|
+
icon_class="mdi mdi-jira",
|
|
22
|
+
)
|