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.
Files changed (43) hide show
  1. netbox_toolkit_plugin/__init__.py +1 -1
  2. netbox_toolkit_plugin/admin.py +11 -7
  3. netbox_toolkit_plugin/api/mixins.py +20 -16
  4. netbox_toolkit_plugin/api/schemas.py +53 -74
  5. netbox_toolkit_plugin/api/serializers.py +10 -11
  6. netbox_toolkit_plugin/api/urls.py +2 -1
  7. netbox_toolkit_plugin/api/views/__init__.py +4 -3
  8. netbox_toolkit_plugin/api/views/command_logs.py +80 -73
  9. netbox_toolkit_plugin/api/views/commands.py +140 -134
  10. netbox_toolkit_plugin/connectors/__init__.py +9 -9
  11. netbox_toolkit_plugin/connectors/base.py +30 -31
  12. netbox_toolkit_plugin/connectors/factory.py +22 -26
  13. netbox_toolkit_plugin/connectors/netmiko_connector.py +18 -28
  14. netbox_toolkit_plugin/connectors/scrapli_connector.py +17 -16
  15. netbox_toolkit_plugin/exceptions.py +0 -7
  16. netbox_toolkit_plugin/filtersets.py +26 -42
  17. netbox_toolkit_plugin/forms.py +13 -11
  18. netbox_toolkit_plugin/migrations/0008_remove_parsed_data_storage.py +26 -0
  19. netbox_toolkit_plugin/models.py +2 -17
  20. netbox_toolkit_plugin/navigation.py +3 -0
  21. netbox_toolkit_plugin/search.py +12 -9
  22. netbox_toolkit_plugin/services/__init__.py +1 -1
  23. netbox_toolkit_plugin/services/command_service.py +7 -10
  24. netbox_toolkit_plugin/services/device_service.py +40 -32
  25. netbox_toolkit_plugin/services/rate_limiting_service.py +4 -3
  26. netbox_toolkit_plugin/{config.py → settings.py} +17 -7
  27. netbox_toolkit_plugin/static/netbox_toolkit_plugin/js/toolkit.js +245 -119
  28. netbox_toolkit_plugin/tables.py +10 -1
  29. netbox_toolkit_plugin/templates/netbox_toolkit_plugin/commandlog.html +16 -84
  30. netbox_toolkit_plugin/templates/netbox_toolkit_plugin/device_toolkit.html +37 -33
  31. netbox_toolkit_plugin/urls.py +10 -3
  32. netbox_toolkit_plugin/utils/connection.py +54 -54
  33. netbox_toolkit_plugin/utils/error_parser.py +128 -109
  34. netbox_toolkit_plugin/utils/logging.py +1 -0
  35. netbox_toolkit_plugin/utils/network.py +74 -47
  36. netbox_toolkit_plugin/views.py +51 -22
  37. {netbox_toolkit_plugin-0.1.1.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/METADATA +2 -2
  38. netbox_toolkit_plugin-0.1.3.dist-info/RECORD +61 -0
  39. netbox_toolkit_plugin-0.1.1.dist-info/RECORD +0 -60
  40. {netbox_toolkit_plugin-0.1.1.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/WHEEL +0 -0
  41. {netbox_toolkit_plugin-0.1.1.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/entry_points.txt +0 -0
  42. {netbox_toolkit_plugin-0.1.1.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/licenses/LICENSE +0 -0
  43. {netbox_toolkit_plugin-0.1.1.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/top_level.txt +0 -0
@@ -1,31 +1,33 @@
1
1
  from django import forms
2
- from django.utils.translation import gettext_lazy as _
2
+
3
+ from dcim.models import Platform
3
4
  from netbox.forms import NetBoxModelForm
4
5
  from utilities.forms.fields import DynamicModelChoiceField
5
- from dcim.models import Platform
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 = ('name', 'command', 'description', 'platform', 'command_type', 'tags')
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 = ('command', 'device', 'output', 'username')
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
- help_text="Password for device authentication"
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
+ ]
@@ -1,7 +1,6 @@
1
1
  from django.db import models
2
- from django.core.exceptions import ValidationError
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 added in migration
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
  ),
@@ -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
- ('name', 100),
8
- ('command', 200),
9
- ('description', 500),
9
+ ("name", 100),
10
+ ("command", 200),
11
+ ("description", 500),
10
12
  )
11
- display_attrs = ('platform', 'command_type', 'description')
13
+ display_attrs = ("platform", "command_type", "description")
14
+
12
15
 
13
16
  class CommandLogIndex(SearchIndex):
14
17
  model = CommandLog
15
18
  fields = (
16
- ('command__name', 100),
17
- ('device__name', 150),
18
- ('username', 200),
19
- ('output', 1000),
19
+ ("command__name", 100),
20
+ ("device__name", 150),
21
+ ("username", 200),
22
+ ("output", 1000),
20
23
  )
21
- display_attrs = ('command', 'device', 'success', 'execution_time')
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__ = ['CommandExecutionService', 'DeviceService', 'RateLimitingService']
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 Optional, Any
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, CommandExecutionError
13
- from ..config import ToolkitSettings
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: "PredefinedCommand",
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) -> List[Command]:
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 = str(device.primary_ip.address.ip) if device.primary_ip else device.name
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
- 'hostname': hostname,
46
- 'platform': platform,
47
- 'has_primary_ip': device.primary_ip is not None,
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(device: Device) -> tuple[bool, Optional[str], dict]:
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
- 'has_platform': device.platform is not None,
67
- 'has_primary_ip': device.primary_ip is not None,
68
- 'has_hostname': bool(device.name), # Check if device has a name
69
- 'platform_supported': False, # Will be set below if platform exists
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['has_platform']:
74
- validation_checks['platform_supported'] = True # If platform exists, we consider it supported
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['has_platform']:
84
+
85
+ if not validation_checks["has_platform"]:
81
86
  is_valid = False
82
87
  error_message = "Device has no platform assigned"
83
- elif not validation_checks['has_primary_ip'] and not validation_checks['has_hostname']:
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 datetime, timedelta
2
- from django.utils import timezone
1
+ from datetime import timedelta
2
+
3
3
  from django.conf import settings
4
- from django.contrib.auth.models import User, Group
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 Dict, Any
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) -> Dict[str, int]:
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 = "") -> Dict[str, int]:
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) -> Dict[str, Any]:
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) -> Dict[str, int]:
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) -> Dict[str, Any]:
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) -> Dict[str, Any]:
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", {}