osism 0.20250616.0__py3-none-any.whl → 0.20250627.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 (30) hide show
  1. osism/api.py +49 -5
  2. osism/commands/baremetal.py +23 -3
  3. osism/commands/manage.py +276 -1
  4. osism/commands/reconciler.py +8 -1
  5. osism/commands/sync.py +27 -7
  6. osism/settings.py +1 -0
  7. osism/tasks/conductor/__init__.py +2 -2
  8. osism/tasks/conductor/ironic.py +21 -19
  9. osism/tasks/conductor/sonic/__init__.py +26 -0
  10. osism/tasks/conductor/sonic/bgp.py +87 -0
  11. osism/tasks/conductor/sonic/cache.py +114 -0
  12. osism/tasks/conductor/sonic/config_generator.py +1000 -0
  13. osism/tasks/conductor/sonic/connections.py +389 -0
  14. osism/tasks/conductor/sonic/constants.py +80 -0
  15. osism/tasks/conductor/sonic/device.py +82 -0
  16. osism/tasks/conductor/sonic/exporter.py +226 -0
  17. osism/tasks/conductor/sonic/interface.py +940 -0
  18. osism/tasks/conductor/sonic/sync.py +215 -0
  19. osism/tasks/reconciler.py +12 -2
  20. {osism-0.20250616.0.dist-info → osism-0.20250627.0.dist-info}/METADATA +3 -3
  21. {osism-0.20250616.0.dist-info → osism-0.20250627.0.dist-info}/RECORD +27 -18
  22. {osism-0.20250616.0.dist-info → osism-0.20250627.0.dist-info}/entry_points.txt +3 -0
  23. osism-0.20250627.0.dist-info/licenses/AUTHORS +1 -0
  24. osism-0.20250627.0.dist-info/pbr.json +1 -0
  25. osism/tasks/conductor/sonic.py +0 -1401
  26. osism-0.20250616.0.dist-info/licenses/AUTHORS +0 -1
  27. osism-0.20250616.0.dist-info/pbr.json +0 -1
  28. {osism-0.20250616.0.dist-info → osism-0.20250627.0.dist-info}/WHEEL +0 -0
  29. {osism-0.20250616.0.dist-info → osism-0.20250627.0.dist-info}/licenses/LICENSE +0 -0
  30. {osism-0.20250616.0.dist-info → osism-0.20250627.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,226 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ """Configuration export functions for SONiC."""
4
+
5
+ import json
6
+ import os
7
+ import difflib
8
+ from loguru import logger
9
+ from deepdiff import DeepDiff
10
+
11
+ from osism import utils, settings
12
+ from .device import get_device_hostname
13
+
14
+
15
+ def save_config_to_netbox(device, config, return_diff=False):
16
+ """Save SONiC configuration to NetBox device local context with diff checking.
17
+
18
+ Checks for existing local context and only saves if configuration has changed.
19
+ Logs diff when changes are detected.
20
+
21
+ Args:
22
+ device: NetBox device object
23
+ config: SONiC configuration dictionary
24
+ return_diff (bool, optional): Whether to return diff output. Defaults to False.
25
+
26
+ Returns:
27
+ bool or tuple: If return_diff is False, returns True if config was saved (changed), False if no changes.
28
+ If return_diff is True, returns (changed, diff_output) tuple.
29
+ """
30
+ try:
31
+ # Get existing local context data
32
+ existing_local_context = device.local_context_data or {}
33
+
34
+ # Prepare new local context data
35
+ new_config_data = {"sonic_config": config}
36
+ diff_output = None
37
+
38
+ if existing_local_context:
39
+ # Compare existing local context with new config
40
+
41
+ # Generate diff
42
+ diff = DeepDiff(existing_local_context, new_config_data, ignore_order=True)
43
+
44
+ if not diff:
45
+ logger.info(
46
+ f"No changes detected for SONiC local context of device {device.name}"
47
+ )
48
+ return (False, None) if return_diff else False
49
+
50
+ # Log the unified diff
51
+ logger.info(f"Configuration changes detected for device {device.name}:")
52
+ existing_json = json.dumps(
53
+ existing_local_context, indent=2, sort_keys=True
54
+ ).splitlines()
55
+ new_json = json.dumps(
56
+ new_config_data, indent=2, sort_keys=True
57
+ ).splitlines()
58
+ unified_diff = difflib.unified_diff(
59
+ existing_json,
60
+ new_json,
61
+ fromfile=f"SONiC Config - {device.name} (existing)",
62
+ tofile=f"SONiC Config - {device.name} (new)",
63
+ lineterm="",
64
+ )
65
+ diff_output = "\n".join(unified_diff)
66
+ if diff_output:
67
+ logger.info(f"Diff:\n{diff_output}")
68
+
69
+ # Save diff to device journal log
70
+ try:
71
+ journal_entry = utils.nb.extras.journal_entries.create(
72
+ assigned_object_type="dcim.device",
73
+ assigned_object_id=device.id,
74
+ kind="info",
75
+ comments=f"SONiC Configuration Update\n\n```diff\n{diff_output}\n```",
76
+ )
77
+ logger.info(
78
+ f"Saved configuration diff to journal for device {device.name}"
79
+ )
80
+ except Exception as e:
81
+ logger.error(
82
+ f"Failed to save diff to journal for device {device.name}: {e}"
83
+ )
84
+ else:
85
+ logger.info(f"Diff: {diff}")
86
+
87
+ # Update existing local context
88
+ device.local_context_data = new_config_data
89
+ device.save()
90
+ logger.info(f"Updated SONiC local context for device {device.name}")
91
+ return (True, diff_output) if return_diff else True
92
+ else:
93
+ # Create new local context (no existing context to compare)
94
+ device.local_context_data = new_config_data
95
+ device.save()
96
+ logger.info(
97
+ f"Created new SONiC local context for device {device.name} (first-time configuration)"
98
+ )
99
+ return (True, None) if return_diff else True
100
+
101
+ except Exception as e:
102
+ logger.error(f"Failed to save local context for device {device.name}: {e}")
103
+ return (False, None) if return_diff else False
104
+
105
+
106
+ def export_config_to_file(device, config):
107
+ """Export SONiC configuration to local file with diff checking.
108
+
109
+ Only writes to file if configuration has changed compared to existing file.
110
+
111
+ Args:
112
+ device: NetBox device object
113
+ config: SONiC configuration dictionary
114
+
115
+ Returns:
116
+ bool: True if config was written (changed), False if no changes
117
+ """
118
+ try:
119
+ # Get configuration from settings
120
+ export_dir = settings.SONIC_EXPORT_DIR
121
+ prefix = settings.SONIC_EXPORT_PREFIX
122
+ suffix = settings.SONIC_EXPORT_SUFFIX
123
+ identifier_type = settings.SONIC_EXPORT_IDENTIFIER
124
+
125
+ # Create export directory if it doesn't exist
126
+ os.makedirs(export_dir, exist_ok=True)
127
+
128
+ # Get identifier based on configuration
129
+ if identifier_type == "serial-number":
130
+ # Get serial number from device
131
+ identifier = (
132
+ device.serial if hasattr(device, "serial") and device.serial else None
133
+ )
134
+ if not identifier:
135
+ logger.warning(
136
+ f"Serial number not found for device {device.name}, falling back to hostname"
137
+ )
138
+ identifier = get_device_hostname(device)
139
+ else:
140
+ logger.debug(
141
+ f"Using serial number {identifier} as identifier for device {device.name}"
142
+ )
143
+ else:
144
+ # Default to hostname (inventory_hostname custom field or device name)
145
+ identifier = get_device_hostname(device)
146
+
147
+ # Generate filename: prefix + identifier + suffix
148
+ filename = f"{prefix}{identifier}{suffix}"
149
+ filepath = os.path.join(export_dir, filename)
150
+
151
+ # Check if file exists and compare content
152
+ config_changed = True
153
+ if os.path.exists(filepath):
154
+ try:
155
+ with open(filepath, "r") as f:
156
+ existing_config = json.load(f)
157
+
158
+ # Compare configurations
159
+ diff = DeepDiff(existing_config, config, ignore_order=True)
160
+
161
+ if not diff:
162
+ logger.info(
163
+ f"No changes detected for SONiC config file of device {device.name}"
164
+ )
165
+ config_changed = False
166
+ else:
167
+ logger.info(
168
+ f"Configuration file changes detected for device {device.name}"
169
+ )
170
+
171
+ except (json.JSONDecodeError, IOError) as e:
172
+ logger.warning(
173
+ f"Could not read existing config file {filepath}: {e}. Will overwrite."
174
+ )
175
+ config_changed = True
176
+
177
+ if config_changed:
178
+ # Export configuration to JSON file
179
+ with open(filepath, "w") as f:
180
+ json.dump(config, f, indent=2)
181
+
182
+ logger.info(f"Exported SONiC config for device {device.name} to {filepath}")
183
+
184
+ # Create hostname symlink if using serial number identifier
185
+ if (
186
+ identifier_type == "serial-number"
187
+ and hasattr(device, "serial")
188
+ and device.serial
189
+ ):
190
+ try:
191
+ hostname = get_device_hostname(device)
192
+ hostname_filename = f"{prefix}{hostname}{suffix}"
193
+ hostname_filepath = os.path.join(export_dir, hostname_filename)
194
+
195
+ logger.debug(
196
+ f"Attempting to create symlink: {hostname_filepath} -> {filename}"
197
+ )
198
+ logger.debug(f"Hostname: {hostname}, Serial: {device.serial}")
199
+
200
+ # Create symlink from hostname file to serial number file
201
+ if os.path.exists(hostname_filepath) or os.path.islink(
202
+ hostname_filepath
203
+ ):
204
+ logger.debug(
205
+ f"Removing existing file/symlink: {hostname_filepath}"
206
+ )
207
+ os.remove(hostname_filepath)
208
+
209
+ os.symlink(filename, hostname_filepath)
210
+ logger.info(
211
+ f"Created hostname symlink {hostname_filepath} -> {filename}"
212
+ )
213
+ except Exception as symlink_error:
214
+ logger.error(
215
+ f"Failed to create hostname symlink for device {device.name}: {symlink_error}"
216
+ )
217
+ else:
218
+ logger.debug(
219
+ f"Symlink conditions not met - identifier_type: {identifier_type}, has_serial: {hasattr(device, 'serial')}, serial_value: {getattr(device, 'serial', None)}"
220
+ )
221
+
222
+ return config_changed
223
+
224
+ except Exception as e:
225
+ logger.error(f"Failed to export config for device {device.name}: {e}")
226
+ return False