netbox-toolkit-plugin 0.1.0__py3-none-any.whl → 0.1.1__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.
Files changed (68) hide show
  1. netbox_toolkit_plugin/__init__.py +32 -0
  2. {netbox_toolkit → netbox_toolkit_plugin}/api/serializers.py +71 -35
  3. {netbox_toolkit → netbox_toolkit_plugin}/api/urls.py +3 -3
  4. {netbox_toolkit → netbox_toolkit_plugin}/config.py +80 -73
  5. {netbox_toolkit → netbox_toolkit_plugin}/connectors/factory.py +170 -111
  6. {netbox_toolkit → netbox_toolkit_plugin}/connectors/netmiko_connector.py +242 -179
  7. {netbox_toolkit → netbox_toolkit_plugin}/connectors/scrapli_connector.py +256 -172
  8. netbox_toolkit_plugin/migrations/0001_initial.py +108 -0
  9. netbox_toolkit_plugin/migrations/0002_alter_command_options_alter_command_unique_together_and_more.py +70 -0
  10. {netbox_toolkit → netbox_toolkit_plugin}/migrations/0003_permission_system_update.py +26 -12
  11. {netbox_toolkit → netbox_toolkit_plugin}/migrations/0004_remove_django_permissions.py +27 -29
  12. {netbox_toolkit → netbox_toolkit_plugin}/migrations/0005_alter_command_options_and_more.py +7 -8
  13. {netbox_toolkit → netbox_toolkit_plugin}/migrations/0006_commandlog_parsed_data_commandlog_parsing_success_and_more.py +7 -8
  14. {netbox_toolkit → netbox_toolkit_plugin}/migrations/0007_alter_commandlog_parsing_template.py +6 -4
  15. {netbox_toolkit → netbox_toolkit_plugin}/models.py +31 -32
  16. {netbox_toolkit → netbox_toolkit_plugin}/navigation.py +6 -6
  17. {netbox_toolkit → netbox_toolkit_plugin}/services/command_service.py +188 -128
  18. {netbox_toolkit → netbox_toolkit_plugin}/services/rate_limiting_service.py +104 -97
  19. netbox_toolkit_plugin/tables.py +51 -0
  20. netbox_toolkit_plugin/templates/netbox_toolkit/command.html +108 -0
  21. netbox_toolkit_plugin/templates/netbox_toolkit/command_list.html +12 -0
  22. netbox_toolkit_plugin/templates/netbox_toolkit/commandlog.html +170 -0
  23. netbox_toolkit_plugin/templates/netbox_toolkit/device_toolkit.html +557 -0
  24. {netbox_toolkit/templates/netbox_toolkit → netbox_toolkit_plugin/templates/netbox_toolkit_plugin}/command.html +5 -5
  25. {netbox_toolkit/templates/netbox_toolkit → netbox_toolkit_plugin/templates/netbox_toolkit_plugin}/command_list.html +2 -2
  26. {netbox_toolkit/templates/netbox_toolkit → netbox_toolkit_plugin/templates/netbox_toolkit_plugin}/commandlog.html +2 -2
  27. {netbox_toolkit/templates/netbox_toolkit → netbox_toolkit_plugin/templates/netbox_toolkit_plugin}/device_toolkit.html +6 -6
  28. netbox_toolkit_plugin/urls.py +38 -0
  29. {netbox_toolkit → netbox_toolkit_plugin}/utils/logging.py +20 -19
  30. {netbox_toolkit → netbox_toolkit_plugin}/views.py +251 -169
  31. {netbox_toolkit_plugin-0.1.0.dist-info → netbox_toolkit_plugin-0.1.1.dist-info}/METADATA +2 -2
  32. netbox_toolkit_plugin-0.1.1.dist-info/RECORD +60 -0
  33. netbox_toolkit_plugin-0.1.1.dist-info/entry_points.txt +2 -0
  34. netbox_toolkit_plugin-0.1.1.dist-info/top_level.txt +1 -0
  35. netbox_toolkit/__init__.py +0 -30
  36. netbox_toolkit/migrations/0001_initial.py +0 -54
  37. netbox_toolkit/migrations/0002_alter_command_options_alter_command_unique_together_and_more.py +0 -66
  38. netbox_toolkit/tables.py +0 -37
  39. netbox_toolkit/urls.py +0 -22
  40. netbox_toolkit_plugin-0.1.0.dist-info/RECORD +0 -56
  41. netbox_toolkit_plugin-0.1.0.dist-info/entry_points.txt +0 -2
  42. netbox_toolkit_plugin-0.1.0.dist-info/top_level.txt +0 -1
  43. {netbox_toolkit → netbox_toolkit_plugin}/admin.py +0 -0
  44. {netbox_toolkit → netbox_toolkit_plugin}/api/__init__.py +0 -0
  45. {netbox_toolkit → netbox_toolkit_plugin}/api/mixins.py +0 -0
  46. {netbox_toolkit → netbox_toolkit_plugin}/api/schemas.py +0 -0
  47. {netbox_toolkit → netbox_toolkit_plugin}/api/views/__init__.py +0 -0
  48. {netbox_toolkit → netbox_toolkit_plugin}/api/views/command_logs.py +0 -0
  49. {netbox_toolkit → netbox_toolkit_plugin}/api/views/commands.py +0 -0
  50. {netbox_toolkit → netbox_toolkit_plugin}/connectors/__init__.py +0 -0
  51. {netbox_toolkit → netbox_toolkit_plugin}/connectors/base.py +0 -0
  52. {netbox_toolkit → netbox_toolkit_plugin}/exceptions.py +0 -0
  53. {netbox_toolkit → netbox_toolkit_plugin}/filtersets.py +0 -0
  54. {netbox_toolkit → netbox_toolkit_plugin}/forms.py +0 -0
  55. {netbox_toolkit → netbox_toolkit_plugin}/migrations/__init__.py +0 -0
  56. {netbox_toolkit → netbox_toolkit_plugin}/search.py +0 -0
  57. {netbox_toolkit → netbox_toolkit_plugin}/services/__init__.py +0 -0
  58. {netbox_toolkit → netbox_toolkit_plugin}/services/device_service.py +0 -0
  59. {netbox_toolkit/static/netbox_toolkit → netbox_toolkit_plugin/static/netbox_toolkit_plugin}/css/toolkit.css +0 -0
  60. {netbox_toolkit/static/netbox_toolkit → netbox_toolkit_plugin/static/netbox_toolkit_plugin}/js/toolkit.js +0 -0
  61. {netbox_toolkit/templates/netbox_toolkit → netbox_toolkit_plugin/templates/netbox_toolkit_plugin}/command_edit.html +0 -0
  62. {netbox_toolkit/templates/netbox_toolkit → netbox_toolkit_plugin/templates/netbox_toolkit_plugin}/commandlog_list.html +0 -0
  63. {netbox_toolkit → netbox_toolkit_plugin}/utils/__init__.py +0 -0
  64. {netbox_toolkit → netbox_toolkit_plugin}/utils/connection.py +0 -0
  65. {netbox_toolkit → netbox_toolkit_plugin}/utils/error_parser.py +0 -0
  66. {netbox_toolkit → netbox_toolkit_plugin}/utils/network.py +0 -0
  67. {netbox_toolkit_plugin-0.1.0.dist-info → netbox_toolkit_plugin-0.1.1.dist-info}/WHEEL +0 -0
  68. {netbox_toolkit_plugin-0.1.0.dist-info → netbox_toolkit_plugin-0.1.1.dist-info}/licenses/LICENSE +0 -0
@@ -7,38 +7,40 @@ from ..models import CommandLog
7
7
 
8
8
  class RateLimitingService:
9
9
  """Service for managing command execution rate limiting"""
10
-
10
+
11
11
  def __init__(self):
12
12
  """Initialize the rate limiting service with plugin settings"""
13
- self.plugin_settings = getattr(settings, 'PLUGINS_CONFIG', {}).get('netbox_toolkit', {})
14
-
13
+ self.plugin_settings = getattr(settings, "PLUGINS_CONFIG", {}).get(
14
+ "netbox_toolkit_plugin", {}
15
+ )
16
+
15
17
  def is_rate_limiting_enabled(self):
16
18
  """Check if rate limiting is enabled in plugin settings"""
17
- return self.plugin_settings.get('rate_limiting_enabled', False)
18
-
19
+ return self.plugin_settings.get("rate_limiting_enabled", False)
20
+
19
21
  def get_device_command_limit(self):
20
22
  """Get the maximum number of commands allowed per device within the time window"""
21
- return self.plugin_settings.get('device_command_limit', 10)
22
-
23
+ return self.plugin_settings.get("device_command_limit", 10)
24
+
23
25
  def get_time_window_minutes(self):
24
26
  """Get the time window in minutes for rate limiting"""
25
- return self.plugin_settings.get('time_window_minutes', 5)
26
-
27
+ return self.plugin_settings.get("time_window_minutes", 5)
28
+
27
29
  def get_bypass_users(self):
28
30
  """Get list of usernames that bypass rate limiting"""
29
- return self.plugin_settings.get('bypass_users', [])
30
-
31
+ return self.plugin_settings.get("bypass_users", [])
32
+
31
33
  def get_bypass_groups(self):
32
34
  """Get list of group names that bypass rate limiting"""
33
- return self.plugin_settings.get('bypass_groups', [])
34
-
35
+ return self.plugin_settings.get("bypass_groups", [])
36
+
35
37
  def user_bypasses_rate_limiting(self, user):
36
38
  """
37
39
  Check if a user bypasses rate limiting based on username or group membership
38
-
40
+
39
41
  Args:
40
42
  user: Django User object
41
-
43
+
42
44
  Returns:
43
45
  bool: True if user bypasses rate limiting, False otherwise
44
46
  """
@@ -46,49 +48,49 @@ class RateLimitingService:
46
48
  bypass_users = self.get_bypass_users()
47
49
  if user.username in bypass_users:
48
50
  return True
49
-
51
+
50
52
  # Check if user is in any bypass groups
51
53
  bypass_groups = self.get_bypass_groups()
52
54
  if bypass_groups:
53
- user_groups = user.groups.values_list('name', flat=True)
55
+ user_groups = user.groups.values_list("name", flat=True)
54
56
  if any(group in bypass_groups for group in user_groups):
55
57
  return True
56
-
58
+
57
59
  return False
58
-
60
+
59
61
  def get_recent_command_count(self, device, user=None):
60
62
  """
61
63
  Get the number of successful commands executed on a device within the time window
62
-
64
+
63
65
  Args:
64
66
  device: Device object
65
67
  user: Optional User object to count only commands by this user
66
-
68
+
67
69
  Returns:
68
70
  int: Number of recent successful commands
69
71
  """
70
72
  time_window = self.get_time_window_minutes()
71
73
  cutoff_time = timezone.now() - timedelta(minutes=time_window)
72
-
74
+
73
75
  query = CommandLog.objects.filter(
74
76
  device=device,
75
77
  execution_time__gte=cutoff_time,
76
- success=True # Only count successful commands
78
+ success=True, # Only count successful commands
77
79
  )
78
-
80
+
79
81
  if user:
80
82
  query = query.filter(username=user.username)
81
-
83
+
82
84
  return query.count()
83
-
85
+
84
86
  def check_rate_limit(self, device, user):
85
87
  """
86
88
  Check if a command execution would exceed rate limits
87
-
89
+
88
90
  Args:
89
91
  device: Device object to check rate limits for
90
92
  user: User object attempting to execute the command
91
-
93
+
92
94
  Returns:
93
95
  dict: {
94
96
  'allowed': bool,
@@ -101,128 +103,133 @@ class RateLimitingService:
101
103
  # If rate limiting is disabled, always allow
102
104
  if not self.is_rate_limiting_enabled():
103
105
  return {
104
- 'allowed': True,
105
- 'current_count': 0,
106
- 'limit': self.get_device_command_limit(),
107
- 'time_window_minutes': self.get_time_window_minutes(),
108
- 'reason': 'Rate limiting disabled'
106
+ "allowed": True,
107
+ "current_count": 0,
108
+ "limit": self.get_device_command_limit(),
109
+ "time_window_minutes": self.get_time_window_minutes(),
110
+ "reason": "Rate limiting disabled",
109
111
  }
110
-
112
+
111
113
  # If user bypasses rate limiting, always allow
112
114
  if self.user_bypasses_rate_limiting(user):
113
115
  return {
114
- 'allowed': True,
115
- 'current_count': 0,
116
- 'limit': self.get_device_command_limit(),
117
- 'time_window_minutes': self.get_time_window_minutes(),
118
- 'reason': 'User bypasses rate limiting'
116
+ "allowed": True,
117
+ "current_count": 0,
118
+ "limit": self.get_device_command_limit(),
119
+ "time_window_minutes": self.get_time_window_minutes(),
120
+ "reason": "User bypasses rate limiting",
119
121
  }
120
-
122
+
121
123
  # Check current command count
122
124
  current_count = self.get_recent_command_count(device)
123
125
  limit = self.get_device_command_limit()
124
126
  time_window = self.get_time_window_minutes()
125
-
127
+
126
128
  if current_count >= limit:
127
129
  return {
128
- 'allowed': False,
129
- 'current_count': current_count,
130
- 'limit': limit,
131
- 'time_window_minutes': time_window,
132
- 'reason': f'Rate limit exceeded: {current_count}/{limit} successful commands in last {time_window} minutes'
130
+ "allowed": False,
131
+ "current_count": current_count,
132
+ "limit": limit,
133
+ "time_window_minutes": time_window,
134
+ "reason": f"Rate limit exceeded: {current_count}/{limit} successful commands in last {time_window} minutes",
133
135
  }
134
-
136
+
135
137
  return {
136
- 'allowed': True,
137
- 'current_count': current_count,
138
- 'limit': limit,
139
- 'time_window_minutes': time_window,
140
- 'reason': 'Within rate limits'
138
+ "allowed": True,
139
+ "current_count": current_count,
140
+ "limit": limit,
141
+ "time_window_minutes": time_window,
142
+ "reason": "Within rate limits",
141
143
  }
142
-
144
+
143
145
  def get_rate_limit_status(self, device, user):
144
146
  """
145
147
  Get rate limit status for display in UI
146
-
148
+
147
149
  Args:
148
150
  device: Device object
149
151
  user: User object
150
-
152
+
151
153
  Returns:
152
154
  dict: Rate limit status information for UI display
153
155
  """
154
156
  if not self.is_rate_limiting_enabled():
155
- return {
156
- 'enabled': False,
157
- 'message': 'Rate limiting is disabled'
158
- }
159
-
157
+ return {"enabled": False, "message": "Rate limiting is disabled"}
158
+
160
159
  if self.user_bypasses_rate_limiting(user):
161
160
  return {
162
- 'enabled': True,
163
- 'bypassed': True,
164
- 'message': 'You have unlimited command execution (bypass enabled)'
161
+ "enabled": True,
162
+ "bypassed": True,
163
+ "message": "You have unlimited command execution (bypass enabled)",
165
164
  }
166
-
165
+
167
166
  current_count = self.get_recent_command_count(device)
168
167
  limit = self.get_device_command_limit()
169
168
  time_window = self.get_time_window_minutes()
170
169
  remaining = max(0, limit - current_count)
171
-
170
+
172
171
  # Determine status and appropriate message
173
172
  if current_count >= limit:
174
- status = 'exceeded'
173
+ status = "exceeded"
175
174
  # Get time until reset for exceeded status
176
175
  time_until_reset = self.get_time_until_reset(device)
177
176
  if time_until_reset:
178
177
  minutes_until_reset = int(time_until_reset.total_seconds() / 60) + 1
179
- message = f'Rate limit exceeded! ({current_count}/{limit} successful commands) - Try again in {minutes_until_reset} minutes'
178
+ message = f"Rate limit exceeded! ({current_count}/{limit} successful commands) - Try again in {minutes_until_reset} minutes"
180
179
  else:
181
- message = f'Rate limit exceeded! ({current_count}/{limit} successful commands in last {time_window} minutes)'
182
- elif remaining <= 2 and current_count < limit: # Only warning if we haven't exceeded the limit
183
- status = 'warning'
184
- message = f'{remaining} commands remaining ({current_count}/{limit} successful commands in last {time_window} minutes)'
180
+ message = f"Rate limit exceeded! ({current_count}/{limit} successful commands in last {time_window} minutes)"
181
+ elif (
182
+ remaining <= 2 and current_count < limit
183
+ ): # Only warning if we haven't exceeded the limit
184
+ status = "warning"
185
+ message = f"{remaining} commands remaining ({current_count}/{limit} successful commands in last {time_window} minutes)"
185
186
  else:
186
- status = 'normal'
187
- message = f'{remaining} commands remaining ({current_count}/{limit} successful commands in last {time_window} minutes)'
188
-
187
+ status = "normal"
188
+ message = f"{remaining} commands remaining ({current_count}/{limit} successful commands in last {time_window} minutes)"
189
+
189
190
  return {
190
- 'enabled': True,
191
- 'bypassed': False,
192
- 'current_count': current_count,
193
- 'limit': limit,
194
- 'remaining': remaining,
195
- 'time_window_minutes': time_window,
196
- 'status': status,
197
- 'message': message,
198
- 'is_exceeded': current_count >= limit,
199
- 'is_warning': remaining <= 2 and current_count < limit,
200
- 'time_until_reset': self.get_time_until_reset(device) if current_count >= limit else None,
191
+ "enabled": True,
192
+ "bypassed": False,
193
+ "current_count": current_count,
194
+ "limit": limit,
195
+ "remaining": remaining,
196
+ "time_window_minutes": time_window,
197
+ "status": status,
198
+ "message": message,
199
+ "is_exceeded": current_count >= limit,
200
+ "is_warning": remaining <= 2 and current_count < limit,
201
+ "time_until_reset": self.get_time_until_reset(device)
202
+ if current_count >= limit
203
+ else None,
201
204
  }
202
-
205
+
203
206
  def get_time_until_reset(self, device):
204
207
  """
205
208
  Get the time until the rate limit resets (oldest successful command in window expires)
206
-
209
+
207
210
  Args:
208
211
  device: Device object
209
-
212
+
210
213
  Returns:
211
214
  timedelta or None: Time until oldest successful command expires, None if no recent successful commands
212
215
  """
213
216
  time_window = self.get_time_window_minutes()
214
217
  cutoff_time = timezone.now() - timedelta(minutes=time_window)
215
-
216
- oldest_command = CommandLog.objects.filter(
217
- device=device,
218
- execution_time__gte=cutoff_time,
219
- success=True # Only consider successful commands
220
- ).order_by('execution_time').first()
221
-
218
+
219
+ oldest_command = (
220
+ CommandLog.objects.filter(
221
+ device=device,
222
+ execution_time__gte=cutoff_time,
223
+ success=True, # Only consider successful commands
224
+ )
225
+ .order_by("execution_time")
226
+ .first()
227
+ )
228
+
222
229
  if not oldest_command:
223
230
  return None
224
-
231
+
225
232
  reset_time = oldest_command.execution_time + timedelta(minutes=time_window)
226
233
  time_until_reset = reset_time - timezone.now()
227
-
234
+
228
235
  return time_until_reset if time_until_reset > timedelta(0) else None
@@ -0,0 +1,51 @@
1
+ import django_tables2 as tables
2
+ from netbox.tables import NetBoxTable, columns
3
+ from .models import Command, CommandLog
4
+
5
+
6
+ class CommandTable(NetBoxTable):
7
+ name = tables.Column(
8
+ linkify=("plugins:netbox_toolkit_plugin:command_detail", [tables.A("pk")])
9
+ )
10
+ platform = tables.Column(linkify=True)
11
+ command_type = tables.Column()
12
+
13
+ class Meta(NetBoxTable.Meta):
14
+ model = Command
15
+ fields = ("pk", "id", "name", "platform", "command_type", "description")
16
+ default_columns = ("pk", "name", "platform", "command_type", "description")
17
+
18
+
19
+ class CommandLogTable(NetBoxTable):
20
+ command = tables.Column(
21
+ linkify=(
22
+ "plugins:netbox_toolkit_plugin:command_detail",
23
+ [tables.A("command.pk")],
24
+ )
25
+ )
26
+ device = tables.Column(linkify=True)
27
+ success = tables.BooleanColumn(verbose_name="Status", yesno=("Success", "Failed"))
28
+
29
+ # Remove actions column entirely
30
+ actions = False
31
+
32
+ class Meta(NetBoxTable.Meta):
33
+ model = CommandLog
34
+ fields = (
35
+ "pk",
36
+ "id",
37
+ "command",
38
+ "device",
39
+ "username",
40
+ "execution_time",
41
+ "success",
42
+ "execution_duration",
43
+ )
44
+ default_columns = (
45
+ "pk",
46
+ "command",
47
+ "device",
48
+ "username",
49
+ "execution_time",
50
+ "success",
51
+ )
@@ -0,0 +1,108 @@
1
+ {% extends 'generic/object.html' %}
2
+ {% load helpers %}
3
+ {% load static %}
4
+
5
+ {% block style %}
6
+ <link href="{% static 'netbox_toolkit_plugin/css/toolkit.css' %}" rel="stylesheet">
7
+ {% endblock %}
8
+
9
+ {% block buttons %}
10
+ {% if perms.netbox_toolkit_plugin.change_command %}
11
+ <a href="{% url 'plugins:netbox_toolkit_plugin:command_edit' pk=object.pk %}" class="btn btn-warning">
12
+ <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
13
+ </a>
14
+ {% endif %}
15
+ {% if perms.netbox_toolkit_plugin.delete_command %}
16
+ <a href="{% url 'plugins:netbox_toolkit_plugin:command_delete' pk=object.pk %}" class="btn btn-danger">
17
+ <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
18
+ </a>
19
+ {% endif %}
20
+ {% endblock %}
21
+
22
+ {% block content %}
23
+ <div class="row mb-3">
24
+ <div class="col col-md-6">
25
+ <div class="card">
26
+ <div class="card-header">
27
+ <h3 class="card-title">Command</h3>
28
+ </div>
29
+ <div class="card-body">
30
+ <table class="table table-hover attr-table">
31
+ <tr>
32
+ <th scope="row">Name</th>
33
+ <td>{{ object.name }}</td>
34
+ </tr>
35
+ <tr>
36
+ <th scope="row">Platform</th>
37
+ <td>{{ object.platform|linkify }}</td>
38
+ </tr>
39
+ <tr>
40
+ <th scope="row">Command Type</th>
41
+ <td>
42
+ {{ object.get_command_type_display }}
43
+ {% if can_execute %}
44
+ <span class="badge bg-success ms-2">
45
+ <i class="mdi mdi-check"></i> Executable
46
+ </span>
47
+ {% else %}
48
+ <span class="badge bg-secondary ms-2">
49
+ <i class="mdi mdi-lock"></i> No Execute Permission
50
+ </span>
51
+ {% endif %}
52
+ </td>
53
+ </tr>
54
+ <tr>
55
+ <th scope="row">Description</th>
56
+ <td>{{ object.description|placeholder }}</td>
57
+ </tr>
58
+ </table>
59
+ </div>
60
+ </div>
61
+ {% include 'inc/panels/custom_fields.html' %}
62
+ {% include 'inc/panels/tags.html' %}
63
+ </div>
64
+ <div class="col col-md-6">
65
+ <div class="card">
66
+ <div class="card-header">
67
+ <h3 class="card-title">Command Detail</h3>
68
+ </div>
69
+ <div class="card-body">
70
+ <pre>{{ object.command }}</pre>
71
+ </div>
72
+ </div>
73
+ </div>
74
+ </div>
75
+ <div class="row">
76
+ <div class="col col-md-12">
77
+ <div class="card">
78
+ <div class="card-header">
79
+ <h3 class="card-title">Recent Command Logs</h3>
80
+ </div>
81
+ <div class="card-body">
82
+ <table class="table table-hover table-headings">
83
+ <thead>
84
+ <tr>
85
+ <th>Device</th>
86
+ <th>Username</th>
87
+ <th>Execution Time</th>
88
+ </tr>
89
+ </thead>
90
+ <tbody>
91
+ {% for log in object.logs.all|slice:":5" %}
92
+ <tr>
93
+ <td>{{ log.device|linkify }}</td>
94
+ <td>{{ log.username }}</td>
95
+ <td>{{ log.execution_time }}</td>
96
+ </tr>
97
+ {% empty %}
98
+ <tr>
99
+ <td colspan="3" class="text-muted">No command executions recorded</td>
100
+ </tr>
101
+ {% endfor %}
102
+ </tbody>
103
+ </table>
104
+ </div>
105
+ </div>
106
+ </div>
107
+ </div>
108
+ {% endblock %}
@@ -0,0 +1,12 @@
1
+ {% extends 'generic/object_list.html' %}
2
+ {% load helpers %}
3
+
4
+ {% block title %}Commands{% endblock %}
5
+
6
+ {% block buttons %}
7
+ {% if perms.netbox_toolkit_plugin.add_command %}
8
+ <a href="{% url 'plugins:netbox_toolkit_plugin:command_add' %}" class="btn btn-primary">
9
+ <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Command
10
+ </a>
11
+ {% endif %}
12
+ {% endblock %}
@@ -0,0 +1,170 @@
1
+ {% extends 'generic/object.html' %}
2
+ {% load helpers %}
3
+ {% load static %}
4
+
5
+ {% block style %}
6
+ <link href="{% static 'netbox_toolkit_plugin/css/toolkit.css' %}" rel="stylesheet">
7
+ {% endblock %}
8
+
9
+ {% block content %}
10
+ <div class="row mb-3">
11
+ <div class="col col-md-6">
12
+ <div class="card">
13
+ <div class="card-header">
14
+ <h3 class="card-title">Command Log</h3>
15
+ </div>
16
+ <div class="card-body">
17
+ <table class="table table-hover attr-table">
18
+ <tr>
19
+ <th scope="row">Command</th>
20
+ <td>{{ object.command|linkify }}</td>
21
+ </tr>
22
+ <tr>
23
+ <th scope="row">Device</th>
24
+ <td>{{ object.device|linkify }}</td>
25
+ </tr>
26
+ <tr>
27
+ <th scope="row">Username</th>
28
+ <td>{{ object.username }}</td>
29
+ </tr>
30
+ <tr>
31
+ <th scope="row">Execution Time</th>
32
+ <td>{{ object.execution_time }}</td>
33
+ </tr>
34
+ <tr>
35
+ <th scope="row">Status</th>
36
+ <td>
37
+ {% if object.success %}
38
+ <span class="badge bg-success">Success</span>
39
+ {% else %}
40
+ <span class="badge bg-danger">Failed</span>
41
+ {% endif %}
42
+ </td>
43
+ </tr>
44
+ {% if object.execution_duration %}
45
+ <tr>
46
+ <th scope="row">Duration</th>
47
+ <td>{{ object.execution_duration|floatformat:3 }}s</td>
48
+ </tr>
49
+ {% endif %}
50
+ {% if object.parsing_success %}
51
+ <tr>
52
+ <th scope="row">Parsing Status</th>
53
+ <td>
54
+ <span class="badge bg-success">
55
+ <i class="mdi mdi-check-circle me-1"></i>
56
+ Successfully Parsed
57
+ </span>
58
+ {% if object.parsing_template %}
59
+ <br><small class="text-muted">Template: {{ object.parsing_template }}</small>
60
+ {% endif %}
61
+ </td>
62
+ </tr>
63
+ {% elif object.parsed_data is not None %}
64
+ <tr>
65
+ <th scope="row">Parsing Status</th>
66
+ <td>
67
+ <span class="badge bg-warning">
68
+ <i class="mdi mdi-alert-circle me-1"></i>
69
+ Parsing Attempted
70
+ </span>
71
+ </td>
72
+ </tr>
73
+ {% endif %}
74
+ {% if not object.success and object.error_message %}
75
+ <tr>
76
+ <th scope="row">Error</th>
77
+ <td><code>{{ object.error_message }}</code></td>
78
+ </tr>
79
+ {% endif %}
80
+ </table>
81
+ </div>
82
+ </div>
83
+ </div>
84
+ </div>
85
+ <div class="row">
86
+ <div class="col col-md-12">
87
+ <div class="card">
88
+ <div class="card-header">
89
+ <h3 class="card-title">Command Output</h3>
90
+ </div>
91
+ <div class="card-body">
92
+ <pre>{{ object.output }}</pre>
93
+ </div>
94
+ </div>
95
+ </div>
96
+ </div>
97
+
98
+ {% if object.parsed_data %}
99
+ <div class="row mt-3">
100
+ <div class="col col-md-12">
101
+ <div class="card">
102
+ <div class="card-header">
103
+ <h3 class="card-title">
104
+ <i class="mdi mdi-table me-1"></i>
105
+ Parsed Data
106
+ </h3>
107
+ {% if object.parsing_template %}
108
+ <div class="card-subtitle">
109
+ <small class="text-muted">Template: {{ object.parsing_template }}</small>
110
+ </div>
111
+ {% endif %}
112
+ </div>
113
+ <div class="card-body">
114
+ {% if object.parsed_data|length > 0 %}
115
+ {% if object.parsed_data.0 %}
116
+ <!-- Table format for structured data -->
117
+ <div class="table-responsive">
118
+ <table class="table table-sm table-striped">
119
+ <thead>
120
+ <tr>
121
+ {% for key in object.parsed_data.0.keys %}
122
+ <th>{{ key|title }}</th>
123
+ {% endfor %}
124
+ </tr>
125
+ </thead>
126
+ <tbody>
127
+ {% for row in object.parsed_data %}
128
+ <tr>
129
+ {% for value in row.values %}
130
+ <td>{{ value }}</td>
131
+ {% endfor %}
132
+ </tr>
133
+ {% endfor %}
134
+ </tbody>
135
+ </table>
136
+ </div>
137
+ {% else %}
138
+ <!-- JSON format for other data types -->
139
+ <pre class="bg-light p-3 rounded">{{ object.parsed_data|pprint }}</pre>
140
+ {% endif %}
141
+ {% else %}
142
+ <div class="alert alert-info mb-0">
143
+ <i class="mdi mdi-information-outline me-1"></i>
144
+ No structured data found in the output.
145
+ </div>
146
+ {% endif %}
147
+
148
+ <!-- Copy parsed data button -->
149
+ {% if object.parsed_data|length > 0 %}
150
+ {{ object.parsed_data|json_script:"commandlog-parsed-data-json" }}
151
+ <div class="mt-3">
152
+ <div class="btn-list">
153
+ <button type="button" class="btn btn-sm btn-outline-primary copy-parsed-btn"
154
+ title="Copy parsed data as JSON">
155
+ <i class="mdi mdi-content-copy me-1"></i>Copy Parsed Data
156
+ </button>
157
+ </div>
158
+ </div>
159
+ {% endif %}
160
+ </div>
161
+ </div>
162
+ </div>
163
+ </div>
164
+ {% endif %}
165
+ {% endblock %}
166
+
167
+ {% block javascript %}
168
+ <!-- Load NetBox Toolkit consolidated JavaScript -->
169
+ <script src="{% static 'netbox_toolkit_plugin/js/toolkit.js' %}"></script>
170
+ {% endblock %}