osism 0.20250605.0__py3-none-any.whl → 0.20250621.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.
Files changed (36) hide show
  1. osism/commands/baremetal.py +71 -10
  2. osism/commands/manage.py +25 -1
  3. osism/commands/netbox.py +22 -5
  4. osism/commands/reconciler.py +6 -28
  5. osism/commands/sync.py +48 -1
  6. osism/commands/validate.py +7 -30
  7. osism/commands/wait.py +8 -31
  8. osism/services/listener.py +1 -1
  9. osism/settings.py +13 -2
  10. osism/tasks/__init__.py +8 -40
  11. osism/tasks/conductor/__init__.py +8 -1
  12. osism/tasks/conductor/ironic.py +90 -66
  13. osism/tasks/conductor/netbox.py +267 -6
  14. osism/tasks/conductor/sonic/__init__.py +26 -0
  15. osism/tasks/conductor/sonic/bgp.py +87 -0
  16. osism/tasks/conductor/sonic/cache.py +114 -0
  17. osism/tasks/conductor/sonic/config_generator.py +908 -0
  18. osism/tasks/conductor/sonic/connections.py +389 -0
  19. osism/tasks/conductor/sonic/constants.py +79 -0
  20. osism/tasks/conductor/sonic/device.py +82 -0
  21. osism/tasks/conductor/sonic/exporter.py +226 -0
  22. osism/tasks/conductor/sonic/interface.py +789 -0
  23. osism/tasks/conductor/sonic/sync.py +190 -0
  24. osism/tasks/conductor.py +2 -0
  25. osism/tasks/netbox.py +6 -4
  26. osism/tasks/reconciler.py +4 -5
  27. osism/utils/__init__.py +51 -4
  28. {osism-0.20250605.0.dist-info → osism-0.20250621.0.dist-info}/METADATA +4 -4
  29. {osism-0.20250605.0.dist-info → osism-0.20250621.0.dist-info}/RECORD +35 -25
  30. {osism-0.20250605.0.dist-info → osism-0.20250621.0.dist-info}/entry_points.txt +5 -0
  31. osism-0.20250621.0.dist-info/pbr.json +1 -0
  32. osism-0.20250605.0.dist-info/pbr.json +0 -1
  33. {osism-0.20250605.0.dist-info → osism-0.20250621.0.dist-info}/WHEEL +0 -0
  34. {osism-0.20250605.0.dist-info → osism-0.20250621.0.dist-info}/licenses/AUTHORS +0 -0
  35. {osism-0.20250605.0.dist-info → osism-0.20250621.0.dist-info}/licenses/LICENSE +0 -0
  36. {osism-0.20250605.0.dist-info → osism-0.20250621.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,87 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ """BGP and AS calculation functions for SONiC configuration."""
4
+
5
+ from loguru import logger
6
+
7
+ from .constants import DEFAULT_LOCAL_AS_PREFIX
8
+
9
+
10
+ def calculate_local_asn_from_ipv4(
11
+ ipv4_address: str, prefix: int = DEFAULT_LOCAL_AS_PREFIX
12
+ ) -> int:
13
+ """Calculate AS number from IPv4 address.
14
+
15
+ Args:
16
+ ipv4_address: IPv4 address in format "192.168.45.123/32" or "192.168.45.123"
17
+ prefix: Four-digit prefix for AS number (default: 4200)
18
+
19
+ Returns:
20
+ AS number calculated as prefix + 3rd octet (padded) + 4th octet (padded)
21
+ Example: 192.168.45.123 with prefix 4200 -> 4200045123
22
+
23
+ Raises:
24
+ ValueError: If IP address format is invalid
25
+ """
26
+ try:
27
+ # Remove CIDR notation if present
28
+ ip_only = ipv4_address.split("/")[0]
29
+ octets = ip_only.split(".")
30
+
31
+ if len(octets) != 4:
32
+ raise ValueError(f"Invalid IPv4 address format: {ipv4_address}")
33
+
34
+ # AS = prefix + third octet (3 digits) + fourth octet (3 digits)
35
+ # Example: 192.168.45.123 -> 4200 + 045 + 123 = 4200045123
36
+ third_octet = int(octets[2])
37
+ fourth_octet = int(octets[3])
38
+
39
+ if not (0 <= third_octet <= 255 and 0 <= fourth_octet <= 255):
40
+ raise ValueError(f"Invalid octet values in: {ipv4_address}")
41
+
42
+ return int(f"{prefix}{third_octet:03d}{fourth_octet:03d}")
43
+ except (IndexError, ValueError) as e:
44
+ raise ValueError(f"Failed to calculate AS from {ipv4_address}: {str(e)}")
45
+
46
+
47
+ # Deprecated: Use connections.find_interconnected_devices instead
48
+ # This function is kept for backward compatibility but delegates to the new module
49
+ def find_interconnected_spine_groups(devices, target_roles=["spine", "superspine"]):
50
+ """Find groups of interconnected spine/superspine switches.
51
+
52
+ Args:
53
+ devices: List of NetBox device objects
54
+ target_roles: List of device roles to consider (default: ["spine", "superspine"])
55
+
56
+ Returns:
57
+ List of groups, where each group is a list of interconnected devices of the same role
58
+ """
59
+ # Import here to avoid circular imports
60
+ from .connections import find_interconnected_devices
61
+
62
+ return find_interconnected_devices(devices, target_roles)
63
+
64
+
65
+ def calculate_minimum_as_for_group(device_group, prefix=DEFAULT_LOCAL_AS_PREFIX):
66
+ """Calculate the minimum AS number for a group of interconnected devices.
67
+
68
+ Args:
69
+ device_group: List of interconnected devices
70
+ prefix: AS prefix (default: DEFAULT_LOCAL_AS_PREFIX)
71
+
72
+ Returns:
73
+ int: Minimum AS number for the group, or None if no valid AS can be calculated
74
+ """
75
+ as_numbers = []
76
+
77
+ for device in device_group:
78
+ if device.primary_ip4:
79
+ try:
80
+ as_number = calculate_local_asn_from_ipv4(
81
+ str(device.primary_ip4), prefix
82
+ )
83
+ as_numbers.append(as_number)
84
+ except ValueError as e:
85
+ logger.debug(f"Could not calculate AS for device {device.name}: {e}")
86
+
87
+ return min(as_numbers) if as_numbers else None
@@ -0,0 +1,114 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ """Interface caching for SONiC configuration generation."""
4
+
5
+ import threading
6
+ from typing import Dict, List, Optional
7
+ from loguru import logger
8
+
9
+ from osism import utils
10
+
11
+
12
+ class InterfaceCache:
13
+ """Thread-local cache for device interfaces during sync_sonic task."""
14
+
15
+ def __init__(self):
16
+ self._cache: Dict[int, List] = {}
17
+ self._lock = threading.Lock()
18
+
19
+ def get_device_interfaces(self, device_id: int) -> List:
20
+ """Get interfaces for a device, using cache if available.
21
+
22
+ Args:
23
+ device_id: NetBox device ID
24
+
25
+ Returns:
26
+ List of interface objects
27
+ """
28
+ with self._lock:
29
+ if device_id not in self._cache:
30
+ logger.debug(f"Fetching interfaces for device {device_id}")
31
+ try:
32
+ interfaces = list(
33
+ utils.nb.dcim.interfaces.filter(device_id=device_id)
34
+ )
35
+ self._cache[device_id] = interfaces
36
+ logger.debug(
37
+ f"Cached {len(interfaces)} interfaces for device {device_id}"
38
+ )
39
+ except Exception as e:
40
+ logger.warning(
41
+ f"Failed to fetch interfaces for device {device_id}: {e}"
42
+ )
43
+ self._cache[device_id] = []
44
+ else:
45
+ logger.debug(f"Using cached interfaces for device {device_id}")
46
+
47
+ return self._cache[device_id]
48
+
49
+ def clear(self):
50
+ """Clear the cache."""
51
+ with self._lock:
52
+ cache_size = len(self._cache)
53
+ self._cache.clear()
54
+ logger.debug(f"Cleared interface cache ({cache_size} devices)")
55
+
56
+ def get_cache_stats(self) -> Dict[str, int]:
57
+ """Get cache statistics.
58
+
59
+ Returns:
60
+ Dictionary with cache statistics
61
+ """
62
+ with self._lock:
63
+ total_interfaces = sum(
64
+ len(interfaces) for interfaces in self._cache.values()
65
+ )
66
+ return {
67
+ "cached_devices": len(self._cache),
68
+ "total_interfaces": total_interfaces,
69
+ }
70
+
71
+
72
+ # Thread-local storage for the interface cache
73
+ _thread_local = threading.local()
74
+
75
+
76
+ def get_interface_cache() -> InterfaceCache:
77
+ """Get the current thread's interface cache.
78
+
79
+ Returns:
80
+ InterfaceCache instance for current thread
81
+ """
82
+ if not hasattr(_thread_local, "interface_cache"):
83
+ _thread_local.interface_cache = InterfaceCache()
84
+ return _thread_local.interface_cache
85
+
86
+
87
+ def get_cached_device_interfaces(device_id: int) -> List:
88
+ """Get interfaces for a device using the thread-local cache.
89
+
90
+ Args:
91
+ device_id: NetBox device ID
92
+
93
+ Returns:
94
+ List of interface objects
95
+ """
96
+ cache = get_interface_cache()
97
+ return cache.get_device_interfaces(device_id)
98
+
99
+
100
+ def clear_interface_cache():
101
+ """Clear the current thread's interface cache."""
102
+ if hasattr(_thread_local, "interface_cache"):
103
+ _thread_local.interface_cache.clear()
104
+
105
+
106
+ def get_interface_cache_stats() -> Optional[Dict[str, int]]:
107
+ """Get cache statistics for the current thread.
108
+
109
+ Returns:
110
+ Dictionary with cache statistics or None if no cache exists
111
+ """
112
+ if hasattr(_thread_local, "interface_cache"):
113
+ return _thread_local.interface_cache.get_cache_stats()
114
+ return None