netbox-toolkit-plugin 0.1.2__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 (42) 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 +21 -25
  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 +6 -9
  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/static/netbox_toolkit_plugin/js/toolkit.js +245 -119
  27. netbox_toolkit_plugin/tables.py +10 -1
  28. netbox_toolkit_plugin/templates/netbox_toolkit_plugin/commandlog.html +16 -84
  29. netbox_toolkit_plugin/templates/netbox_toolkit_plugin/device_toolkit.html +37 -33
  30. netbox_toolkit_plugin/urls.py +10 -3
  31. netbox_toolkit_plugin/utils/connection.py +54 -54
  32. netbox_toolkit_plugin/utils/error_parser.py +128 -109
  33. netbox_toolkit_plugin/utils/logging.py +1 -0
  34. netbox_toolkit_plugin/utils/network.py +74 -47
  35. netbox_toolkit_plugin/views.py +51 -22
  36. {netbox_toolkit_plugin-0.1.2.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/METADATA +2 -2
  37. netbox_toolkit_plugin-0.1.3.dist-info/RECORD +61 -0
  38. netbox_toolkit_plugin-0.1.2.dist-info/RECORD +0 -60
  39. {netbox_toolkit_plugin-0.1.2.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/WHEEL +0 -0
  40. {netbox_toolkit_plugin-0.1.2.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/entry_points.txt +0 -0
  41. {netbox_toolkit_plugin-0.1.2.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/licenses/LICENSE +0 -0
  42. {netbox_toolkit_plugin-0.1.2.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,15 +1,15 @@
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
11
+ from ..exceptions import DeviceConnectionError
12
+ from ..models import Command, CommandLog
13
13
  from ..settings import ToolkitSettings
14
14
  from ..utils.logging import get_toolkit_logger
15
15
 
@@ -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