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.
- osism/api.py +49 -5
- osism/commands/baremetal.py +23 -3
- osism/commands/manage.py +276 -1
- osism/commands/reconciler.py +8 -1
- osism/commands/sync.py +27 -7
- osism/settings.py +1 -0
- osism/tasks/conductor/__init__.py +2 -2
- osism/tasks/conductor/ironic.py +21 -19
- osism/tasks/conductor/sonic/__init__.py +26 -0
- osism/tasks/conductor/sonic/bgp.py +87 -0
- osism/tasks/conductor/sonic/cache.py +114 -0
- osism/tasks/conductor/sonic/config_generator.py +1000 -0
- osism/tasks/conductor/sonic/connections.py +389 -0
- osism/tasks/conductor/sonic/constants.py +80 -0
- osism/tasks/conductor/sonic/device.py +82 -0
- osism/tasks/conductor/sonic/exporter.py +226 -0
- osism/tasks/conductor/sonic/interface.py +940 -0
- osism/tasks/conductor/sonic/sync.py +215 -0
- osism/tasks/reconciler.py +12 -2
- {osism-0.20250616.0.dist-info → osism-0.20250627.0.dist-info}/METADATA +3 -3
- {osism-0.20250616.0.dist-info → osism-0.20250627.0.dist-info}/RECORD +27 -18
- {osism-0.20250616.0.dist-info → osism-0.20250627.0.dist-info}/entry_points.txt +3 -0
- osism-0.20250627.0.dist-info/licenses/AUTHORS +1 -0
- osism-0.20250627.0.dist-info/pbr.json +1 -0
- osism/tasks/conductor/sonic.py +0 -1401
- osism-0.20250616.0.dist-info/licenses/AUTHORS +0 -1
- osism-0.20250616.0.dist-info/pbr.json +0 -1
- {osism-0.20250616.0.dist-info → osism-0.20250627.0.dist-info}/WHEEL +0 -0
- {osism-0.20250616.0.dist-info → osism-0.20250627.0.dist-info}/licenses/LICENSE +0 -0
- {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
|