netbox-toolkit-plugin 0.1.1__py3-none-any.whl → 0.1.3__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_toolkit_plugin/__init__.py +1 -1
- netbox_toolkit_plugin/admin.py +11 -7
- netbox_toolkit_plugin/api/mixins.py +20 -16
- netbox_toolkit_plugin/api/schemas.py +53 -74
- netbox_toolkit_plugin/api/serializers.py +10 -11
- netbox_toolkit_plugin/api/urls.py +2 -1
- netbox_toolkit_plugin/api/views/__init__.py +4 -3
- netbox_toolkit_plugin/api/views/command_logs.py +80 -73
- netbox_toolkit_plugin/api/views/commands.py +140 -134
- netbox_toolkit_plugin/connectors/__init__.py +9 -9
- netbox_toolkit_plugin/connectors/base.py +30 -31
- netbox_toolkit_plugin/connectors/factory.py +22 -26
- netbox_toolkit_plugin/connectors/netmiko_connector.py +18 -28
- netbox_toolkit_plugin/connectors/scrapli_connector.py +17 -16
- netbox_toolkit_plugin/exceptions.py +0 -7
- netbox_toolkit_plugin/filtersets.py +26 -42
- netbox_toolkit_plugin/forms.py +13 -11
- netbox_toolkit_plugin/migrations/0008_remove_parsed_data_storage.py +26 -0
- netbox_toolkit_plugin/models.py +2 -17
- netbox_toolkit_plugin/navigation.py +3 -0
- netbox_toolkit_plugin/search.py +12 -9
- netbox_toolkit_plugin/services/__init__.py +1 -1
- netbox_toolkit_plugin/services/command_service.py +7 -10
- netbox_toolkit_plugin/services/device_service.py +40 -32
- netbox_toolkit_plugin/services/rate_limiting_service.py +4 -3
- netbox_toolkit_plugin/{config.py → settings.py} +17 -7
- netbox_toolkit_plugin/static/netbox_toolkit_plugin/js/toolkit.js +245 -119
- netbox_toolkit_plugin/tables.py +10 -1
- netbox_toolkit_plugin/templates/netbox_toolkit_plugin/commandlog.html +16 -84
- netbox_toolkit_plugin/templates/netbox_toolkit_plugin/device_toolkit.html +37 -33
- netbox_toolkit_plugin/urls.py +10 -3
- netbox_toolkit_plugin/utils/connection.py +54 -54
- netbox_toolkit_plugin/utils/error_parser.py +128 -109
- netbox_toolkit_plugin/utils/logging.py +1 -0
- netbox_toolkit_plugin/utils/network.py +74 -47
- netbox_toolkit_plugin/views.py +51 -22
- {netbox_toolkit_plugin-0.1.1.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/METADATA +2 -2
- netbox_toolkit_plugin-0.1.3.dist-info/RECORD +61 -0
- netbox_toolkit_plugin-0.1.1.dist-info/RECORD +0 -60
- {netbox_toolkit_plugin-0.1.1.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/WHEEL +0 -0
- {netbox_toolkit_plugin-0.1.1.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/entry_points.txt +0 -0
- {netbox_toolkit_plugin-0.1.1.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/licenses/LICENSE +0 -0
- {netbox_toolkit_plugin-0.1.1.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/top_level.txt +0 -0
netbox_toolkit_plugin/forms.py
CHANGED
@@ -1,31 +1,33 @@
|
|
1
1
|
from django import forms
|
2
|
-
|
2
|
+
|
3
|
+
from dcim.models import Platform
|
3
4
|
from netbox.forms import NetBoxModelForm
|
4
5
|
from utilities.forms.fields import DynamicModelChoiceField
|
5
|
-
|
6
|
+
|
6
7
|
from .models import Command, CommandLog
|
7
8
|
|
9
|
+
|
8
10
|
class CommandForm(NetBoxModelForm):
|
9
11
|
platform = DynamicModelChoiceField(
|
10
12
|
queryset=Platform.objects.all(),
|
11
|
-
help_text="Platform this command is designed for (e.g., cisco_ios, cisco_nxos, generic)"
|
13
|
+
help_text="Platform this command is designed for (e.g., cisco_ios, cisco_nxos, generic)",
|
12
14
|
)
|
13
|
-
|
15
|
+
|
14
16
|
class Meta:
|
15
17
|
model = Command
|
16
|
-
fields = (
|
18
|
+
fields = ("name", "command", "description", "platform", "command_type", "tags")
|
19
|
+
|
17
20
|
|
18
21
|
class CommandLogForm(NetBoxModelForm):
|
19
22
|
class Meta:
|
20
23
|
model = CommandLog
|
21
|
-
fields = (
|
24
|
+
fields = ("command", "device", "output", "username")
|
25
|
+
|
22
26
|
|
23
27
|
class CommandExecutionForm(forms.Form):
|
24
28
|
username = forms.CharField(
|
25
|
-
max_length=100,
|
26
|
-
help_text="Username for device authentication"
|
29
|
+
max_length=100, help_text="Username for device authentication"
|
27
30
|
)
|
28
31
|
password = forms.CharField(
|
29
|
-
widget=forms.PasswordInput,
|
30
|
-
|
31
|
-
)
|
32
|
+
widget=forms.PasswordInput, help_text="Password for device authentication"
|
33
|
+
)
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# Generated migration to remove parsed data storage
|
2
|
+
|
3
|
+
from django.db import migrations
|
4
|
+
|
5
|
+
|
6
|
+
class Migration(migrations.Migration):
|
7
|
+
|
8
|
+
dependencies = [
|
9
|
+
('netbox_toolkit_plugin', '0007_alter_commandlog_parsing_template'),
|
10
|
+
]
|
11
|
+
|
12
|
+
operations = [
|
13
|
+
# Remove parsed data fields since we now parse fresh from raw output
|
14
|
+
migrations.RemoveField(
|
15
|
+
model_name='commandlog',
|
16
|
+
name='parsed_data',
|
17
|
+
),
|
18
|
+
migrations.RemoveField(
|
19
|
+
model_name='commandlog',
|
20
|
+
name='parsing_success',
|
21
|
+
),
|
22
|
+
migrations.RemoveField(
|
23
|
+
model_name='commandlog',
|
24
|
+
name='parsing_template',
|
25
|
+
),
|
26
|
+
]
|
netbox_toolkit_plugin/models.py
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
from django.db import models
|
2
|
-
|
2
|
+
|
3
3
|
from netbox.models import NetBoxModel
|
4
|
-
from dcim.models import Device
|
5
4
|
|
6
5
|
|
7
6
|
class Command(NetBoxModel):
|
@@ -55,27 +54,13 @@ class CommandLog(NetBoxModel):
|
|
55
54
|
username = models.CharField(max_length=100)
|
56
55
|
execution_time = models.DateTimeField(auto_now_add=True)
|
57
56
|
|
58
|
-
# Execution details
|
57
|
+
# Execution details
|
59
58
|
success = models.BooleanField(default=True)
|
60
59
|
error_message = models.TextField(blank=True)
|
61
60
|
execution_duration = models.FloatField(
|
62
61
|
blank=True, null=True, help_text="Command execution time in seconds"
|
63
62
|
)
|
64
63
|
|
65
|
-
# Parsing details for TextFSM
|
66
|
-
parsed_data = models.JSONField(
|
67
|
-
blank=True, null=True, help_text="Parsed structured data from command output"
|
68
|
-
)
|
69
|
-
parsing_success = models.BooleanField(
|
70
|
-
default=False, help_text="Whether the output was successfully parsed"
|
71
|
-
)
|
72
|
-
parsing_template = models.CharField(
|
73
|
-
max_length=255,
|
74
|
-
blank=True,
|
75
|
-
null=True,
|
76
|
-
help_text="Name of the TextFSM template used for parsing",
|
77
|
-
)
|
78
|
-
|
79
64
|
def __str__(self):
|
80
65
|
return f"{self.command} on {self.device}"
|
81
66
|
|
@@ -11,18 +11,21 @@ menu = PluginMenu(
|
|
11
11
|
PluginMenuItem(
|
12
12
|
link="plugins:netbox_toolkit_plugin:command_list",
|
13
13
|
link_text="Commands",
|
14
|
+
permissions=["netbox_toolkit_plugin.view_command"],
|
14
15
|
buttons=(
|
15
16
|
PluginMenuButton(
|
16
17
|
"plugins:netbox_toolkit_plugin:command_add",
|
17
18
|
"Add",
|
18
19
|
"mdi mdi-plus-thick",
|
19
20
|
ButtonColorChoices.GRAY,
|
21
|
+
permissions=["netbox_toolkit_plugin.add_command"],
|
20
22
|
),
|
21
23
|
),
|
22
24
|
),
|
23
25
|
PluginMenuItem(
|
24
26
|
link="plugins:netbox_toolkit_plugin:commandlog_list",
|
25
27
|
link_text="Command Logs",
|
28
|
+
permissions=["netbox_toolkit_plugin.view_commandlog"],
|
26
29
|
),
|
27
30
|
),
|
28
31
|
),
|
netbox_toolkit_plugin/search.py
CHANGED
@@ -1,21 +1,24 @@
|
|
1
1
|
from netbox.search import SearchIndex
|
2
|
+
|
2
3
|
from .models import Command, CommandLog
|
3
4
|
|
5
|
+
|
4
6
|
class CommandIndex(SearchIndex):
|
5
7
|
model = Command
|
6
8
|
fields = (
|
7
|
-
(
|
8
|
-
(
|
9
|
-
(
|
9
|
+
("name", 100),
|
10
|
+
("command", 200),
|
11
|
+
("description", 500),
|
10
12
|
)
|
11
|
-
display_attrs = (
|
13
|
+
display_attrs = ("platform", "command_type", "description")
|
14
|
+
|
12
15
|
|
13
16
|
class CommandLogIndex(SearchIndex):
|
14
17
|
model = CommandLog
|
15
18
|
fields = (
|
16
|
-
(
|
17
|
-
(
|
18
|
-
(
|
19
|
-
(
|
19
|
+
("command__name", 100),
|
20
|
+
("device__name", 150),
|
21
|
+
("username", 200),
|
22
|
+
("output", 1000),
|
20
23
|
)
|
21
|
-
display_attrs = (
|
24
|
+
display_attrs = ("command", "device", "success", "execution_time")
|
@@ -4,4 +4,4 @@ from .command_service import CommandExecutionService
|
|
4
4
|
from .device_service import DeviceService
|
5
5
|
from .rate_limiting_service import RateLimitingService
|
6
6
|
|
7
|
-
__all__ = [
|
7
|
+
__all__ = ["CommandExecutionService", "DeviceService", "RateLimitingService"]
|
@@ -1,16 +1,16 @@
|
|
1
1
|
"""Service for handling command execution on devices."""
|
2
2
|
|
3
3
|
import traceback
|
4
|
-
from typing import
|
4
|
+
from typing import Any
|
5
5
|
|
6
6
|
from dcim.models import Device
|
7
7
|
|
8
|
-
from ..models import Command, CommandLog
|
9
|
-
from ..connectors.factory import ConnectorFactory
|
10
8
|
from ..connectors.base import CommandResult
|
9
|
+
from ..connectors.factory import ConnectorFactory
|
11
10
|
from ..connectors.netmiko_connector import NetmikoConnector
|
12
|
-
from ..exceptions import DeviceConnectionError
|
13
|
-
from ..
|
11
|
+
from ..exceptions import DeviceConnectionError
|
12
|
+
from ..models import Command, CommandLog
|
13
|
+
from ..settings import ToolkitSettings
|
14
14
|
from ..utils.logging import get_toolkit_logger
|
15
15
|
|
16
16
|
logger = get_toolkit_logger(__name__)
|
@@ -24,7 +24,7 @@ class CommandExecutionService:
|
|
24
24
|
|
25
25
|
def execute_command_with_retry(
|
26
26
|
self,
|
27
|
-
command: "
|
27
|
+
command: "Command",
|
28
28
|
device: Any,
|
29
29
|
username: str,
|
30
30
|
password: str,
|
@@ -231,7 +231,7 @@ class CommandExecutionService:
|
|
231
231
|
success = False
|
232
232
|
error_message = result.error_message or ""
|
233
233
|
|
234
|
-
# Create log entry with execution details
|
234
|
+
# Create log entry with execution details (raw output only)
|
235
235
|
command_log = CommandLog.objects.create(
|
236
236
|
command=command,
|
237
237
|
device=device,
|
@@ -240,9 +240,6 @@ class CommandExecutionService:
|
|
240
240
|
success=success,
|
241
241
|
error_message=error_message,
|
242
242
|
execution_duration=result.execution_time,
|
243
|
-
parsed_data=result.parsed_output,
|
244
|
-
parsing_success=result.parsing_success,
|
245
|
-
parsing_template=result.parsing_method,
|
246
243
|
)
|
247
244
|
|
248
245
|
if result.has_syntax_error:
|
@@ -1,5 +1,4 @@
|
|
1
1
|
"""Service for device-related operations."""
|
2
|
-
from typing import List, Optional
|
3
2
|
|
4
3
|
from dcim.models import Device
|
5
4
|
|
@@ -8,80 +7,89 @@ from ..models import Command
|
|
8
7
|
|
9
8
|
class DeviceService:
|
10
9
|
"""Service for device-related operations."""
|
11
|
-
|
10
|
+
|
12
11
|
@staticmethod
|
13
|
-
def get_available_commands(device: Device) ->
|
12
|
+
def get_available_commands(device: Device) -> list[Command]:
|
14
13
|
"""
|
15
14
|
Get all commands available for a device based on its platform.
|
16
|
-
|
15
|
+
|
17
16
|
Args:
|
18
17
|
device: The device to get commands for
|
19
|
-
|
18
|
+
|
20
19
|
Returns:
|
21
20
|
List of available commands
|
22
21
|
"""
|
23
22
|
if not device.platform:
|
24
23
|
return []
|
25
|
-
|
24
|
+
|
26
25
|
commands = Command.objects.filter(platform=device.platform)
|
27
|
-
|
26
|
+
|
28
27
|
return list(commands)
|
29
|
-
|
28
|
+
|
30
29
|
@staticmethod
|
31
30
|
def get_device_connection_info(device: Device) -> dict:
|
32
31
|
"""
|
33
32
|
Get connection information for a device.
|
34
|
-
|
33
|
+
|
35
34
|
Args:
|
36
35
|
device: The device to get connection info for
|
37
|
-
|
36
|
+
|
38
37
|
Returns:
|
39
38
|
Dictionary with connection information
|
40
39
|
"""
|
41
|
-
hostname =
|
40
|
+
hostname = (
|
41
|
+
str(device.primary_ip.address.ip) if device.primary_ip else device.name
|
42
|
+
)
|
42
43
|
platform = str(device.platform).lower() if device.platform else None
|
43
|
-
|
44
|
+
|
44
45
|
info = {
|
45
|
-
|
46
|
-
|
47
|
-
|
46
|
+
"hostname": hostname,
|
47
|
+
"platform": platform,
|
48
|
+
"has_primary_ip": device.primary_ip is not None,
|
48
49
|
}
|
49
|
-
|
50
|
+
|
50
51
|
return info
|
51
|
-
|
52
|
+
|
52
53
|
@staticmethod
|
53
|
-
def validate_device_for_commands(
|
54
|
+
def validate_device_for_commands(
|
55
|
+
device: Device,
|
56
|
+
) -> tuple[bool, str | None, dict]:
|
54
57
|
"""
|
55
58
|
Validate if a device is ready for command execution.
|
56
|
-
|
59
|
+
|
57
60
|
Args:
|
58
61
|
device: The device to validate
|
59
|
-
|
62
|
+
|
60
63
|
Returns:
|
61
64
|
Tuple of (is_valid, error_message, validation_checks)
|
62
65
|
validation_checks is a dict with check names as keys and booleans as values
|
63
66
|
"""
|
64
67
|
# Initialize validation checks
|
65
68
|
validation_checks = {
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
69
|
+
"has_platform": device.platform is not None,
|
70
|
+
"has_primary_ip": device.primary_ip is not None,
|
71
|
+
"has_hostname": bool(device.name), # Check if device has a name
|
72
|
+
"platform_supported": False, # Will be set below if platform exists
|
70
73
|
}
|
71
|
-
|
74
|
+
|
72
75
|
# Additional logic for more complex checks
|
73
|
-
if validation_checks[
|
74
|
-
validation_checks[
|
75
|
-
|
76
|
+
if validation_checks["has_platform"]:
|
77
|
+
validation_checks["platform_supported"] = (
|
78
|
+
True # If platform exists, we consider it supported
|
79
|
+
)
|
80
|
+
|
76
81
|
# Determine overall validity and error message
|
77
82
|
is_valid = True
|
78
83
|
error_message = None
|
79
|
-
|
80
|
-
if not validation_checks[
|
84
|
+
|
85
|
+
if not validation_checks["has_platform"]:
|
81
86
|
is_valid = False
|
82
87
|
error_message = "Device has no platform assigned"
|
83
|
-
elif
|
88
|
+
elif (
|
89
|
+
not validation_checks["has_primary_ip"]
|
90
|
+
and not validation_checks["has_hostname"]
|
91
|
+
):
|
84
92
|
is_valid = False
|
85
93
|
error_message = "Device has no primary IP address or hostname"
|
86
|
-
|
94
|
+
|
87
95
|
return is_valid, error_message, validation_checks
|
@@ -1,7 +1,8 @@
|
|
1
|
-
from datetime import
|
2
|
-
|
1
|
+
from datetime import timedelta
|
2
|
+
|
3
3
|
from django.conf import settings
|
4
|
-
from django.
|
4
|
+
from django.utils import timezone
|
5
|
+
|
5
6
|
from ..models import CommandLog
|
6
7
|
|
7
8
|
|
@@ -1,8 +1,18 @@
|
|
1
1
|
"""Configuration settings for the NetBox Toolkit plugin."""
|
2
2
|
|
3
|
-
from typing import
|
3
|
+
from typing import Any
|
4
|
+
|
4
5
|
from django.conf import settings
|
5
6
|
|
7
|
+
# Plugin metadata - required by NetBox's plugin discovery system
|
8
|
+
__version__ = "0.1.1"
|
9
|
+
__author__ = "Andy Norwood"
|
10
|
+
|
11
|
+
# Make these available as module-level attributes for NetBox's plugin system
|
12
|
+
version = __version__
|
13
|
+
author = __author__
|
14
|
+
release_track = "stable" # or "beta", "alpha" - indicates the release track
|
15
|
+
|
6
16
|
|
7
17
|
class ToolkitSettings:
|
8
18
|
"""Configuration class for toolkit settings."""
|
@@ -104,7 +114,7 @@ class ToolkitSettings:
|
|
104
114
|
}
|
105
115
|
|
106
116
|
@classmethod
|
107
|
-
def get_fast_test_timeouts(cls) ->
|
117
|
+
def get_fast_test_timeouts(cls) -> dict[str, int]:
|
108
118
|
"""Get fast connection test timeouts for initial viability testing."""
|
109
119
|
return cls.FAST_TEST_TIMEOUTS.copy()
|
110
120
|
|
@@ -117,7 +127,7 @@ class ToolkitSettings:
|
|
117
127
|
)
|
118
128
|
|
119
129
|
@classmethod
|
120
|
-
def get_timeouts_for_device(cls, device_type_model: str = "") ->
|
130
|
+
def get_timeouts_for_device(cls, device_type_model: str = "") -> dict[str, int]:
|
121
131
|
"""Get timeout configuration for a specific device type."""
|
122
132
|
timeouts = cls.DEFAULT_TIMEOUTS.copy()
|
123
133
|
|
@@ -140,17 +150,17 @@ class ToolkitSettings:
|
|
140
150
|
return cls.PLATFORM_ALIASES.get(platform_lower, platform_lower)
|
141
151
|
|
142
152
|
@classmethod
|
143
|
-
def get_ssh_options(cls) ->
|
153
|
+
def get_ssh_options(cls) -> dict[str, Any]:
|
144
154
|
"""Get SSH transport options."""
|
145
155
|
return cls.SSH_TRANSPORT_OPTIONS.copy()
|
146
156
|
|
147
157
|
@classmethod
|
148
|
-
def get_retry_config(cls) ->
|
158
|
+
def get_retry_config(cls) -> dict[str, int]:
|
149
159
|
"""Get retry configuration."""
|
150
160
|
return cls.RETRY_CONFIG.copy()
|
151
161
|
|
152
162
|
@classmethod
|
153
|
-
def get_ssh_transport_options(cls) ->
|
163
|
+
def get_ssh_transport_options(cls) -> dict[str, Any]:
|
154
164
|
"""Get SSH transport options for Scrapli."""
|
155
165
|
user_config = getattr(settings, "PLUGINS_CONFIG", {}).get(
|
156
166
|
"netbox_toolkit_plugin", {}
|
@@ -158,7 +168,7 @@ class ToolkitSettings:
|
|
158
168
|
return {**cls.SSH_TRANSPORT_OPTIONS, **user_config.get("ssh_options", {})}
|
159
169
|
|
160
170
|
@classmethod
|
161
|
-
def get_netmiko_config(cls) ->
|
171
|
+
def get_netmiko_config(cls) -> dict[str, Any]:
|
162
172
|
"""Get Netmiko configuration for fallback connections."""
|
163
173
|
user_config = getattr(settings, "PLUGINS_CONFIG", {}).get(
|
164
174
|
"netbox_toolkit_plugin", {}
|