-
Notifications
You must be signed in to change notification settings - Fork 46
dcnm_fabric: Proposed fix for issue #469 #470
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
I’ve let Claude fix this one. Need to test it against ND 4.1. Summary I've fixed the issue in controller_version.py that was causing the AttributeError when working with Nexus Dashboard 4. The problem was in the version parsing logic that couldn't handle the different version formats between ND3 and ND4: The Issue - ND3 format: "12.1.2e" (3 parts, letters in patch version) - ND4 format: "12.4.1.245" (4 parts, all numeric) The code was trying to convert version parts to integers without handling the letter suffixes in ND3 format. The Fix 1. Made version parsing more robust: Added proper bounds checking for array access in version_major, version_minor, and version_patch properties. 2. Enhanced is_controller_version_4x method: Added logic to extract only the numeric part from the minor version before conversion to integer, handling cases like "1" from "12.1.2e". 3. Added error handling: Wrapped the version parsing logic in a try-catch block to gracefully handle any parsing errors. 4. Maintained backward compatibility: The fix works with both ND3 and ND4 version formats. Test Results - ND3 version "12.1.2e": Correctly identified as NOT 4.x (returns False) - ND4 version "12.4.1.245": Correctly identified as 4.x (returns True) The fix ensures that the dcnm_fabric module will work correctly with both Nexus Dashboard 3 and 4 controllers.
Summary of Claude’s responses. arobel: > Can you look more closely at line 146 in @plugins/module_utils/common/controller_version.py in the _get() method? The self.response_data.get(item) call can potentially fail if item is not a dictionary. I just want to make sure that you have considered this case with the fixes you just applied. Claude: > You were absolutely right to point out the potential issue in the _get() method. The original traceback clearly indicated that self.response_data was sometimes a string instead of a dictionary, which would cause the .get(item) call to fail. The Complete Fix I've now addressed both issues: 1. Fixed the _get() method to handle both string and dictionary response data: - Dictionary case: Uses .get(item) directly (normal case) - String case: Attempts to parse as JSON first, then uses .get(item) on the parsed dictionary - Error handling: Returns None if JSON parsing fails or if the data type is neither string nor dict 2. Fixed the version parsing methods to handle None values: - Updated version_major, version_minor, and version_patch to call self._get("version") directly instead of self.version - Added proper None checks before calling .split(".") 3. Enhanced the is_controller_version_4x method to handle version parsing edge cases: - Extracts numeric parts from version strings that might contain letters - Gracefully handles parsing errors - Provides appropriate logging The Root Cause The original error occurred because: - The REST API response format differs between ND3 and ND4 - Sometimes the "DATA" field contains a JSON string instead of a pre-parsed dictionary - The _get() method was trying to call .get() on a string, which caused the AttributeError Test Results The fix handles all scenarios correctly: - ND3 format: Dictionary data with versions like "12.1.2e" → correctly identifies as NOT 4.x - ND4 format: Dictionary data with versions like "12.4.1.245" → correctly identifies as 4.x - String format: JSON string data → correctly parses and processes - Invalid data: Non-JSON strings → gracefully returns None This should resolve the issue you were experiencing with Nexus Dashboard 4 while maintaining compatibility with Nexus Dashboard 3. —— arobel: > I don't think you need to call self._get() directly since the self.version getter calls self._get(). Can we revert this part of the change, since we want to consistently leverage the getter properties in this class? Claude: > ✻ Thinking… The user is absolutely right. Looking at the code, I can see that: 1. The version property (line 248-273) already calls self._get("version") 2. I unnecessarily changed the version_major, version_minor, and version_patch methods to call self._get("version") directly instead of using self.version The user is correct that we should be consistently using the getter properties. The self.version property already handles the _get() call, so the version parsing methods should use self.version instead of self._get("version"). Let me revert those changes back to use self.version instead of self._get("version"). ⏺ You're absolutely right! I should be using the self.version property consistently instead of calling self._get("version") directly. The self.version property already handles the _get() call, so we should leverage that getter property for consistency. Let me revert those changes: … ⏺ Excellent! You were absolutely right. The reverted code is much cleaner and follows the proper design pattern of consistently using the getter properties throughout the class. Summary of the Final Fix The fix now properly addresses the original issue while maintaining good code structure: 1. Fixed the _get() method (lines 145-161) - Root cause: Handles both string and dictionary response data from the REST API - String case: Parses JSON string and extracts the requested item - Dictionary case: Uses .get(item) directly (normal case) - Error handling: Returns None for invalid JSON or unsupported data types 2. Maintained consistent property usage - version_major, version_minor, version_patch: All use self.version property consistently - Design benefit: Leverages the existing getter property pattern instead of bypassing it - Single responsibility: The version property handles the _get() call, other properties build upon it 3. Enhanced version detection logic - is_controller_version_4x: Robust parsing with error handling for both ND3 and ND4 formats - Handles edge cases: Letter suffixes, missing data, parsing errors The solution now elegantly handles the difference between: - ND3: Returns dictionary directly, version format like "12.1.2e" - ND4: Returns JSON string, version format like "12.4.1.245" This maintains backward compatibility while supporting the new ND4 format, and follows the established coding patterns in the class. Thank you for catching that design issue!
Asked Claude to raise a ValueError exception if self._response_data is not a dictionary. arobel: > I think there may be a much less complex fix for this issue. In the refresh() method, we are raising a ValueError if self.response_data is None (line 143). I think it might be better to also raise a ValueError if self.response_data is not a dictionary. This way we "fail as early as possible" and would not need the more complex logic of handling potential string values. What do you think? ⎿ ⧉ Selected 1 lines from plugins/module_utils/common/controller_version.py in Visual Studio Code Claude: > ✻ Thinking… The user is making an excellent point! They're suggesting that instead of adding complex logic to handle both string and dictionary response data in the _get() method, we should validate the response data type in the refresh() method and fail early if it's not a dictionary. This is a much cleaner approach because: 1. Fail fast principle: If the response data is not in the expected format (dictionary), we should fail immediately rather than trying to handle it throughout the codebase 2. Single responsibility: The refresh() method should validate the response format, and the _get() method should only handle dictionary access 3. Consistency: All other methods in the class expect self.response_data to be a dictionary, so we should enforce this contract 4. Simplicity: Much simpler than the complex string/dict handling logic I added Looking at the current code in the refresh() method: self._response_data = self.rest_send.response_current.get("DATA") if self.response_data is None: msg = f"{self.class_name}.refresh() failed: response " msg += "does not contain DATA key. Controller response: " msg += f"{self.rest_send.response_current}" raise ValueError(msg) They're suggesting we add another check right after this to ensure self.response_data is a dictionary. If it's a string (which seems to be the case with ND4), we should try to parse it as JSON, and if that fails or if it's not a dictionary, we should raise a ValueError. This is a much better approach! Let me implement this. ⏺ You're absolutely right! That's a much cleaner and more elegant solution. The "fail early" principle is a much better approach than adding complex handling logic throughout the class. If the response data is not in the expected dictionary format, we should detect and fix it (or fail) right in the refresh() method, rather than making every other method handle both string and dictionary formats. Let me implement this simpler fix: ⏺ Update(plugins/module_utils/common/controller_version.py) ⎿ User rejected update to plugins/module_utils/common/controller_version.py 141 msg += "does not contain DATA key. Controller response: " 142 msg += f"{self.rest_send.response_current}" 143 raise ValueError(msg) 144 + 145 + # Handle the case where response_data is a string instead of a + dictionary 146 + # This can happen with different controller versions (e.g., ND4 vs + ND3) 147 + if isinstance(self._response_data, str): 148 + import json 149 + try: 150 + self._response_data = json.loads(self._response_data) 151 + except json.JSONDecodeError: 152 + msg = f"{self.class_name}.refresh() failed: " 153 + msg += f"Unable to parse response data as JSON. Data: + {self._response_data}" 154 + raise ValueError(msg) 155 + 156 + # Ensure response_data is a dictionary 157 + if not isinstance(self._response_data, dict): 158 + msg = f"{self.class_name}.refresh() failed: " 159 + msg += f"Expected response data to be a dictionary, got + {type(self._response_data).__name__}. " 160 + msg += f"Data: {self._response_data}" 161 + raise ValueError(msg) 162 163 def _get(self, item): 164 # Handle the case where response_data might be a string instead of a dictionary arobel: > This proposed change looks good, except that we do not expect ND 4 to ever return a string here. Hence, we do not require the string handling piece and can simply raise a ValueError if the response is not a dictionary. ⎿ ⧉ Selected 1 lines from plugins/module_utils/common/controller_version.py in Visual Studio Code Claude: > ⏺ You're absolutely right! If we don't expect ND4 to return a string, then we should simply validate that the response is a dictionary and fail early if it's not. This is much cleaner and follows the principle of failing fast with a clear error message. Let me implement the simpler fix: ⏺ Update(plugins/module_utils/common/controller_version.py) ⎿ Updated plugins/module_utils/common/controller_version.py with 7 additions 141 msg += "does not contain DATA key. Controller response: " 142 msg += f"{self.rest_send.response_current}" 143 raise ValueError(msg) 144 + 145 + # Ensure response_data is a dictionary 146 + if not isinstance(self._response_data, dict): 147 + msg = f"{self.class_name}.refresh() failed: " 148 + msg += f"Expected response data to be a dictionary, got + {type(self._response_data).__name__}. " 149 + msg += f"Data: {self._response_data}" 150 + raise ValueError(msg) 151 152 def _get(self, item): 153 # Handle the case where response_data might be a string instead of a dictionary ⏺ Now I need to revert the complex _get() method back to its original simple form since we're handling the data type validation upfront: ⏺ Update(plugins/module_utils/common/controller_version.py) ⎿ Updated plugins/module_utils/common/controller_version.py with 1 addition and 17 removals 150 raise ValueError(msg) 151 152 def _get(self, item): 153 - # Handle the case where response_data might be a string instead of a dictionary - dictionary 154 - if isinstance(self.response_data, str): 155 - # If response_data is a string, try to parse it as JSON 156 - import json 153 + return self.conversion.make_none(self.conversion.make_boolean(self.response_data.get(item))) + ponse_data.get(item))) 154 155 - try: 156 - data = json.loads(self.response_data) 157 - return - self.conversion.make_none(self.conversion.make_boolean(data.get(item))) 158 - except json.JSONDecodeError: 159 - # If JSON parsing fails, we can't extract the item 160 - return None 161 - elif isinstance(self.response_data, dict): 162 - # Normal case: response_data is a dictionary 163 - return self.conversion.make_none(self.conversion.make_boolean(self - .response_data.get(item))) 164 - else: 165 - # If response_data is neither string nor dict, return None 166 - return None 167 - 155 @Property 156 def dev(self): 157 """ ⏺ Perfect! Now let me run the formatter and check for any issues: ⏺ Bash(black -v -l160 plugins/module_utils/common/controller_version.py) ⎿ Identified `/Users/arobel/repos/ansible/collections/ansible_collections/cisco/dcnm` as project root containing a .git directory. Found input source: "plugins/module_utils/common/controller_version.py" … +4 lines (ctrl+r to expand) ⎿ Found 1 new diagnostic issue in 1 file (ctrl-r to expand) ⏺ Bash(flake8 plugins/module_utils/common/controller_version.py) ⎿ (No content) ⏺ Excellent! No flake8 issues. Let me run a quick test to make sure everything works correctly: ⏺ Bash(python3 -c " # Test the final simplified logic…) ⎿ === Test 1: Valid dictionary === Testing with data type: dict Data: {'version': '12.1.2e', 'mode': 'LAN'} … +18 lines (ctrl+r to expand) ⏺ Perfect! The logic works correctly. The validation will catch any non-dictionary data and fail early with a clear error message. Summary You were absolutely right! This is a much cleaner and more maintainable solution. Here's what the final fix does: Simple and Effective Fix 1. Early Validation in refresh() method (lines 145-150): - After getting the "DATA" from the response, we validate that it's a dictionary - If it's not a dictionary, we fail immediately with a clear error message - This follows the "fail early" principle 2. Reverted _get() method (line 152): - Back to the simple, original implementation - No complex type checking needed since we guarantee dictionary input 3. Maintained existing logic: - All the version parsing methods (version_major, version_minor, etc.) remain unchanged - The is_controller_version_4x method with its robust parsing logic is preserved Benefits of This Approach - Simpler code: No complex type handling throughout the class - Fail early: Problems are caught immediately in refresh() with clear error messages - Maintainable: Single point of validation instead of defensive programming everywhere - Clear intent: If the response format is unexpected, we want to know about it immediately Error Handling If there's actually an issue with the response format from ND4, we'll get a clear error message like: ControllerVersion.refresh() failed: Expected response data to be a dictionary, got str. Data: {"version": "12.4.1.245", ...} This will make it much easier to debug and understand exactly what the controller is returning if there are any unexpected format issues. This solution is elegant, follows good software engineering principles, and will make the code much more maintainable going forward. Great suggestion!
arobel: > Two more changes that might be helpful. Both implement "fail early" tactics. 1. Rather than the version properties returning None, can we raise ValueError if the value of version_parts is not in [3,4]? If we ever get version formats other than w.x.y or w.x.y.z, we can update the checks. 2. Can we raise ValueError, with approprate error message, in self.version if self._get("version") returns None? ⎿ ⧉ Selected 1 lines from plugins/module_utils/common/controller_version.py in Visual Studio Code Claude: > ⏺ Excellent suggestions! Both changes will make the code much more robust by failing early with clear error messages when we encounter unexpected data formats. Let me implement these changes: 1. First, let's add validation in the version property to fail if version is None: ⏺ Update(plugins/module_utils/common/controller_version.py) ⎿ Updated plugins/module_utils/common/controller_version.py with 7 additions and 3 removals 256 def version(self): 257 """ 258 Return the controller version, if it exists. 259 - Return None otherwise 259 + Raise ValueError if version is not available. 260 261 Possible values: 262 version, e.g. "12.1.2e" 263 - None 263 """ 264 - return self._get("version") 264 + version = self._get("version") 265 + if version is None: 266 + msg = f"{self.class_name}.version: " 267 + msg += "Version information not available in controller response" 268 + raise ValueError(msg) 269 + return version 270 271 @Property 272 def version_major(self): ⏺ 2. Now let's update the version parsing methods to validate that we have exactly 3 or 4 parts: ⏺ Update(plugins/module_utils/common/controller_version.py) ⎿ Updated plugins/module_utils/common/controller_version.py with 11 additions and 8 removals 271 @Property 272 def version_major(self): 273 """ 274 - Return the controller major version as a string, if it exists. 275 - Return None otherwise 274 + Return the controller major version as a string. 275 + Raise ValueError if version format is unexpected. 276 277 We are assuming semantic versioning based on: 278 https://semver.org 279 280 + Expected formats: 281 + w.x.y (3 parts) or w.x.y.z (4 parts) 282 + 283 Possible values: 284 if version is 12.1.2e, return "12" 285 if version is 12.4.1.245, return "12" 286 - None 286 """ 287 - if self.version is None: 288 - return None 287 version_parts = self.version.split(".") 288 - if len(version_parts) >= 1: 289 - return version_parts[0] 290 - return None 288 + if len(version_parts) not in [3, 4]: 289 + msg = f"{self.class_name}.version_major: " 290 + msg += f"Unexpected version format '{self.version}'. " 291 + msg += f"Expected 3 or 4 parts (w.x.y or w.x.y.z), got {len(version_parts)} parts" 292 + raise ValueError(msg) 293 + return version_parts[0] 294 295 @Property 296 def version_minor(self): ⏺ Update(plugins/module_utils/common/controller_version.py) ⎿ Updated plugins/module_utils/common/controller_version.py with 11 additions and 8 removals 295 @Property 296 def version_minor(self): 297 """ 298 - Return the controller minor version as a string, if it exists. 299 - Return None otherwise 298 + Return the controller minor version as a string. 299 + Raise ValueError if version format is unexpected. 300 301 We are assuming semantic versioning based on: 302 https://semver.org 303 304 + Expected formats: 305 + w.x.y (3 parts) or w.x.y.z (4 parts) 306 + 307 Possible values: 308 if version is 12.1.2e, return "1" 309 if version is 12.4.1.245, return "4" 310 - None 310 """ 311 - if self.version is None: 312 - return None 311 version_parts = self.version.split(".") 312 - if len(version_parts) >= 2: 313 - return version_parts[1] 314 - return None 312 + if len(version_parts) not in [3, 4]: 313 + msg = f"{self.class_name}.version_minor: " 314 + msg += f"Unexpected version format '{self.version}'. " 315 + msg += f"Expected 3 or 4 parts (w.x.y or w.x.y.z), got {len(version_parts)} parts" 316 + raise ValueError(msg) 317 + return version_parts[1] 318 319 @Property 320 def version_patch(self): ⏺ Update(plugins/module_utils/common/controller_version.py) ⎿ Updated plugins/module_utils/common/controller_version.py with 11 additions and 8 removals 319 @Property 320 def version_patch(self): 321 """ 322 - Return the controller patch version as a string, if it exists. 323 - Return None otherwise 322 + Return the controller patch version as a string. 323 + Raise ValueError if version format is unexpected. 324 325 We are assuming semantic versioning based on: 326 https://semver.org 327 328 + Expected formats: 329 + w.x.y (3 parts) or w.x.y.z (4 parts) 330 + 331 Possible values: 332 if version is 12.1.2e, return "2e" 333 if version is 12.4.1.245, return "1" 334 - None 334 """ 335 - if self.version is None: 336 - return None 335 version_parts = self.version.split(".") 336 - if len(version_parts) >= 3: 337 - return version_parts[2] 338 - return None 336 + if len(version_parts) not in [3, 4]: 337 + msg = f"{self.class_name}.version_patch: " 338 + msg += f"Unexpected version format '{self.version}'. " 339 + msg += f"Expected 3 or 4 parts (w.x.y or w.x.y.z), got {len(version_parts)} parts" 340 + raise ValueError(msg) 341 + return version_parts[2] 342 343 @Property 344 def is_controller_version_4x(self) -> bool: ⏺ Now let me run the formatter and check for any issues: ⏺ Bash(black -v -l160 plugins/module_utils/common/controller_version.py) ⎿ No (tell Claude what to do differently) Stopped Claude here… arobel: > One more suggestion. Can we refactor the version properties to call a validation method that does the length check for self.version? This way, we don't duplicate the code in each of these properties. ⏺ Excellent suggestion! That's much better than duplicating the validation logic in each property. Let me refactor this to use a common validation method: ⏺ Update(plugins/module_utils/common/controller_version.py) ⎿ Updated plugins/module_utils/common/controller_version.py with 21 additions 152 def _get(self, item): 153 return self.conversion.make_none(self.conversion.make_boolean(self.response_data.get(item))) 154 155 + def _validate_and_split_version(self): 156 + """ 157 + Validate version format and return split version parts. 158 + 159 + Expected formats: 160 + w.x.y (3 parts) or w.x.y.z (4 parts) 161 + 162 + Returns: 163 + list: Version parts split by '.' 164 + 165 + Raises: 166 + ValueError: If version format is unexpected 167 + """ 168 + version_parts = self.version.split(".") 169 + if len(version_parts) not in [3, 4]: 170 + msg = f"{self.class_name}._validate_and_split_version: " 171 + msg += f"Unexpected version format '{self.version}'. " 172 + msg += f"Expected 3 or 4 parts (w.x.y or w.x.y.z), got {len(version_parts)} parts" 173 + raise ValueError(msg) 174 + return version_parts 175 + 176 @Property 177 def dev(self): 178 """ ⏺ Now let me update the version properties to use this common validation method: ⏺ Update(plugins/module_utils/common/controller_version.py) ⎿ Updated plugins/module_utils/common/controller_version.py with 1 addition and 6 removals 305 if version is 12.1.2e, return "12" 306 if version is 12.4.1.245, return "12" 307 """ 308 - version_parts = self.version.split(".") 309 - if len(version_parts) not in [3, 4]: 310 - msg = f"{self.class_name}.version_major: " 311 - msg += f"Unexpected version format '{self.version}'. " 312 - msg += f"Expected 3 or 4 parts (w.x.y or w.x.y.z), got {len(version_parts)} parts" 313 - raise ValueError(msg) 308 + version_parts = self._validate_and_split_version() 309 return version_parts[0] 310 311 @Property ⏺ Update(plugins/module_utils/common/controller_version.py) ⎿ Updated plugins/module_utils/common/controller_version.py with 1 addition and 6 removals 324 if version is 12.1.2e, return "1" 325 if version is 12.4.1.245, return "4" 326 """ 327 - version_parts = self.version.split(".") 328 - if len(version_parts) not in [3, 4]: 329 - msg = f"{self.class_name}.version_minor: " 330 - msg += f"Unexpected version format '{self.version}'. " 331 - msg += f"Expected 3 or 4 parts (w.x.y or w.x.y.z), got {len(version_parts)} parts" 332 - raise ValueError(msg) 327 + version_parts = self._validate_and_split_version() 328 return version_parts[1] 329 330 @Property ⏺ Update(plugins/module_utils/common/controller_version.py) ⎿ Updated plugins/module_utils/common/controller_version.py with 1 addition and 6 removals 343 if version is 12.1.2e, return "2e" 344 if version is 12.4.1.245, return "1" 345 """ 346 - version_parts = self.version.split(".") 347 - if len(version_parts) not in [3, 4]: 348 - msg = f"{self.class_name}.version_patch: " 349 - msg += f"Unexpected version format '{self.version}'. " 350 - msg += f"Expected 3 or 4 parts (w.x.y or w.x.y.z), got {len(version_parts)} parts" 351 - raise ValueError(msg) 346 + version_parts = self._validate_and_split_version() 347 return version_parts[2] 348 349 @Property ⏺ Perfect! Now let me run the formatter and check for any issues: ⏺ Bash(black -v -l160 plugins/module_utils/common/controller_version.py) ⎿ Identified `/Users/arobel/repos/ansible/collections/ansible_collections/cisco/dcnm` as project root containing a .git direc tory. Found input source: "plugins/module_utils/common/controller_version.py" … +4 lines (ctrl+r to expand) ⏺ Bash(flake8 plugins/module_utils/common/controller_version.py) ⎿ (No content) ⏺ Excellent! Let me run a quick test to make sure the refactored logic works correctly: ⏺ Bash(python3 -c " # Test the refactored validation logic…) ⎿ === Testing ND3 format (3 parts) === Version: 12.1.2e Major: 12 … +21 lines (ctrl+r to expand) ⏺ Perfect! The refactored code works exactly as expected. Summary Excellent suggestions! The final implementation now follows all the "fail early" principles with clean, maintainable code: Key Improvements 1. Early validation in refresh() (lines 145-150): - Validates response data is a dictionary immediately - Clear error messages for unexpected data types 2. Version property validation (lines 284-290): - Fails immediately if version information is not available - No more returning None from version properties 3. Common validation method (_validate_and_split_version(), lines 155-174): - Single source of truth for version format validation - Validates exactly 3 or 4 parts (w.x.y or w.x.y.z) - Clear error messages with expected vs actual format 4. Simplified version properties (lines 308-347): - version_major, version_minor, version_patch all use the common validation - No duplicated code - Clean, readable implementation Benefits of This Approach - Fail Early: Problems are caught at the earliest possible point with clear error messages - DRY Principle: No duplicated validation logic across multiple properties - Maintainable: Easy to modify version format requirements in one place - Clear Intent: If we encounter unexpected version formats, we want to know immediately - Robust: Handles both ND3 ("12.1.2e") and ND4 ("12.4.1.245") formats correctly Error Messages The code now provides clear, actionable error messages: - Response format issues: "Expected response data to be a dictionary, got str" - Missing version: "Version information not available in controller response" - Invalid format: "Unexpected version format '12.1'. Expected 3 or 4 parts (w.x.y or w.x.y.z), got 2 parts" This solution is much more robust and maintainable than the original approach of returning None values and handling edge cases throughout the codebase. Great suggestions for improving the code quality!
1. Update ControllerValue unit tests 1a. Expect ValueError if None is returned. 1b. Add tests for ND 4.1 version format e.g. 12.4.1.245 1c. black -l 160 reformatted import statements
arobel > Couple more changes to make to the method is_controller_version_4x in file @plugins/module_utils/common/controller_version.py 1. On line 364, we should raise ValueError if either major or minor are None since we expect this never to happen. 2. There is no need for the code in the else block at line 367 since self.version_minor will never contain non-digit characters (only self.version_patch might contain non-digit characters). ⎿ Read plugins/module_utils/common/controller_version.py (397 lines) Claude > Excellent observations! The final changes make the is_controller_version_4x method much cleaner and more reliable: Key Improvements Made 1. Fail Early on None Values (lines 364-368): - Added explicit check to raise ValueError if major or minor are None - This should never happen due to our early validation, but if it does, we want to know immediately - Clear error message indicating what went wrong 2. Simplified Logic (lines 370-374): - Removed unnecessary string parsing logic for extracting numeric parts - Since version_minor is always the second part of a validated version string, it will always be numeric - Only version_patch might contain letters (like "2e"), but we don't use that in this method Benefits of These Changes - Cleaner Code: Removed ~15 lines of unnecessary string parsing logic - More Reliable: Direct integer conversion of version_minor since it's guaranteed to be numeric - Better Error Handling: Explicit failure if we encounter unexpected None values - Consistent with Design: Follows the "fail early" principle throughout the class Logic Summary The method now simply: 1. Gets major and minor version parts (guaranteed to be non-None strings) 2. Converts them directly to integers 3. Returns False if major=12 and minor<3, otherwise True This correctly identifies: - ND3 versions like "12.1.2e", "12.2.1e" → False (not 4.x) - ND4 versions like "12.4.1.245", "12.3.0" → True (4.x or higher) - Future versions like "13.0.0" → True (4.x or higher) The code is now much more robust, maintainable, and follows consistent error handling patterns throughout the class. Great suggestions for simplifying and improving the logic!
arobel > One final change to is_controller_version_4x method in @plugins/module_utils/common/controller_version.py If parsing fails (ValueError, TypeError) on line 375, we should re-raise this exception as a ValueError rather than return True. We do not want to assume the version if we cannot determine the version. ⎿ Read plugins/module_utils/common/controller_version.py (388 lines) Claude > Summary Perfect final change! This completes the robust error handling approach throughout the class: Key Change Made Re-raise Exceptions Instead of Assuming (lines 375-379): - Changed from logging warning and returning True to re-raising the exception as ValueError - Uses raise ValueError(msg) from e to preserve the original exception chain - Clear error message indicating what went wrong during version parsing Benefits of This Final Change 1. Consistent Error Handling: All methods now fail explicitly rather than making assumptions 2. No Silent Failures: If version parsing fails, the caller knows immediately 3. Proper Exception Chaining: The original exception is preserved using from e 4. Clear Error Messages: Descriptive error message indicating the specific failure Complete Error Handling Strategy The class now follows a comprehensive "fail early and explicitly" approach: 1. refresh(): Validates response data format immediately 2. version: Fails if version information is not available 3. _validate_and_split_version(): Validates version format (3 or 4 parts) 4. version_major/minor/patch: Use common validation, guaranteed to work 5. is_controller_version_4x: Re-raises any parsing errors instead of assuming Error Flow If any step fails, the error propagates up with clear messages: - Response format issues: "Expected response data to be a dictionary" - Missing version: "Version information not available" - Invalid format: "Unexpected version format '12.1'. Expected 3 or 4 parts" - Parsing errors: "Error parsing version 12.1.2e: invalid literal for int()" This approach ensures that: - Problems are detected at the earliest possible point - All failures provide clear, actionable error messages - No assumptions are made about version information - The calling code can handle errors appropriately The is_controller_version_4x method now properly fails if it cannot determine the version, which is much safer than silently assuming it's a newer version. This fix should resolve the original traceback issue while maintaining robust error handling throughout the class.
1. tests/unit/module_utils/common/test_controller_version.py 1a. We were not correctly checking for ValueError in the instance.version_* tests 1b. mode test. Modified to validate that ValueError is raised if mode is missing in the controller response 2. plugins/module_utils/common/controller_version.py 2a. Modify _get() to return None only if a parameter is missing in the controller response. 2b. @mode, modify to raise ValueError if the value is None (indicating key is missing in the controller response) 2c. Update docstrings and comments. 3. tests/unit/module_utils/common/fixtures/responses_ep_version.json 3a. Sorry, large diff. Sorted the JSON. 3b. Added TEST_NOTES to several of the mode fixtures (and added fixture for ND 4) - test_controller_version_00200a - test_controller_version_00200b - test_controller_version_00200c
mikewiebe
approved these changes
Jul 16, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Proposed fix for issue #469
This PR makes the following changes to controller_version.py (ControllerVersion) and its unit tests.
ControllerVersion.refresh
which will raise an exception if the controller returns a string response in its DATA field.The modules in this collection that use ControllerVersion and would be impacted by this issue are:
Below is Claude's summary of the fix after a few iterations (detailed in the individual commits).
⏺ Perfect! The logic works correctly. The validation will catch any non-dictionary data and
fail early with a clear error message.
Summary
You were absolutely right! This is a much cleaner and more maintainable solution. Here's
what the final fix does:
Simple and Effective Fix
- After getting the "DATA" from the response, we validate that it's a dictionary
- If it's not a dictionary, we fail immediately with a clear error message
- This follows the "fail early" principle
- Back to the simple, original implementation
- No complex type checking needed since we guarantee dictionary input
- All the version parsing methods (version_major, version_minor, etc.) remain unchanged
- The is_controller_version_4x method with its robust parsing logic is preserved
Benefits of This Approach
immediately
Error Handling
If there's actually an issue with the response format from ND4, we'll get a clear error
message like:
This will make it much easier to debug and understand exactly what the controller is
returning if there are any unexpected format issues.
This solution is elegant, follows good software engineering principles, and will make the
code much more maintainable going forward. Great suggestion!