Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/managednetworkfabric/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
Release History
===============

9.1.1
++++++
* Fix CLI output formatting for `fabric` and `device` `resync-password` and `fabric` `rotate-certificate` commands to prevent duplicated nested error details in both successful and partial-success responses.

9.1.0
++++++
* Enables the following previously removed command/command groups:
Expand Down
26 changes: 26 additions & 0 deletions src/managednetworkfabric/azext_managednetworkfabric/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ def load_command_table(self, _): # pylint: disable=unused-argument

self.command_table["networkfabric device run-ro"] = RunReadCommand(loader=self)

from .custom.device_resync_password import ( # pylint: disable=no-name-in-module
ResyncPasswordCommand as DeviceResyncPasswordCommand,
)

self.command_table["networkfabric device resync-password"] = (
DeviceResyncPasswordCommand(loader=self)
)

with self.command_group("networkfabric internetgateway"):

from .operations.internetgateway._show import ShowCommand
Expand All @@ -32,3 +40,21 @@ def load_command_table(self, _): # pylint: disable=unused-argument
self.command_table["networkfabric internetgateway list"] = ListCommand(
loader=self
)

with self.command_group("networkfabric fabric"):

from .custom.fabric_resync_password import ( # pylint: disable=no-name-in-module
ResyncPasswordCommand as FabricResyncPasswordCommand,
)

self.command_table["networkfabric fabric resync-password"] = (
FabricResyncPasswordCommand(loader=self)
)

from .custom.fabric_rotate_certificate import ( # pylint: disable=no-name-in-module
RotateCertificateCommand as FabricRotateCertificateCommand,
)

self.command_table["networkfabric fabric rotate-certificate"] = (
FabricRotateCertificateCommand(loader=self)
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------
Comment thread
nafizhaider32 marked this conversation as resolved.

import importlib.util
from pathlib import Path


def _load_legacy_custom_module():
"""Load and return the sibling ``custom.py`` module for backward compatibility."""
legacy_module_path = Path(__file__).resolve().parent.parent / "custom.py"
if not legacy_module_path.is_file():
return None

module_name = __name__ + "._legacy_custom"
spec = importlib.util.spec_from_file_location(module_name, str(legacy_module_path))
if spec is None or spec.loader is None:
return None

module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module


_legacy_custom_module = _load_legacy_custom_module()

if _legacy_custom_module is not None:
for _name in dir(_legacy_custom_module):
if not _name.startswith("_"):
globals()[_name] = getattr(_legacy_custom_module, _name)

__all__ = [name for name in dir(_legacy_custom_module) if not name.startswith("_")]
else:
__all__ = []
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
#
# --------------------------------------------------------------------------------------------
# pylint: disable=protected-access,duplicate-code

"""
This code inherits the auto-generated code for device resync-password command, and adds
custom error formatting.
"""

from azure.core.exceptions import HttpResponseError

from azext_managednetworkfabric.aaz.latest.networkfabric.device import (
ResyncPassword as _ResyncPasswordCommand,
)
from azext_managednetworkfabric.operations.error_format import ErrorFormat


class ResyncPasswordCommand(_ResyncPasswordCommand):
"""Custom class for networkfabric device resync-password"""

def _handler(self, command_args):
poller = super()._handler(command_args)
if poller is None:
return None
if self.ctx.args.no_wait:
return poller
try:
return poller.result()
except HttpResponseError as e:
ErrorFormat.handle_lro_error(e)
Comment thread
nafizhaider32 marked this conversation as resolved.
Comment thread
nafizhaider32 marked this conversation as resolved.

def _output(self, *args, **kwargs):
return ErrorFormat._output(self, *args, **kwargs)
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
#
# --------------------------------------------------------------------------------------------
# pylint: disable=protected-access,duplicate-code

"""
This code inherits the auto-generated code for fabric resync-password command, and adds
custom error formatting.
"""

from azure.core.exceptions import HttpResponseError

from azext_managednetworkfabric.aaz.latest.networkfabric.fabric import (
ResyncPassword as _ResyncPasswordCommand,
)
from azext_managednetworkfabric.operations.error_format import ErrorFormat


class ResyncPasswordCommand(_ResyncPasswordCommand):
"""Custom class for networkfabric fabric resync-password"""

def _handler(self, command_args):
poller = super()._handler(command_args)
if poller is None:
return None
if self.ctx.args.no_wait:
return poller
try:
return poller.result()
except HttpResponseError as e:
ErrorFormat.handle_lro_error(e)
Comment thread
nafizhaider32 marked this conversation as resolved.
Comment thread
nafizhaider32 marked this conversation as resolved.

def _output(self, *args, **kwargs):
return ErrorFormat._output(self, *args, **kwargs)
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
#
# --------------------------------------------------------------------------------------------
# pylint: disable=protected-access,duplicate-code

"""
This code inherits the auto-generated code for fabric rotate-certificate command, and adds
custom error formatting.
"""

from azure.core.exceptions import HttpResponseError

from azext_managednetworkfabric.aaz.latest.networkfabric.fabric import (
RotateCertificate as _RotateCertificateCommand,
)
from azext_managednetworkfabric.operations.error_format import ErrorFormat


class RotateCertificateCommand(_RotateCertificateCommand):
"""Custom class for networkfabric fabric rotate-certificate"""

def _handler(self, command_args):
poller = super()._handler(command_args)
if poller is None:
return None
if self.ctx.args.no_wait:
return poller
try:
return poller.result()
except HttpResponseError as e:
ErrorFormat.handle_lro_error(e)
Comment thread
nafizhaider32 marked this conversation as resolved.
Comment thread
nafizhaider32 marked this conversation as resolved.

def _output(self, *args, **kwargs):
return ErrorFormat._output(self, *args, **kwargs)
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
#
# Custom code that is added in addition to auto-generated by aaz-dev code.
# --------------------------------------------------------------------------------------------
# pylint: disable=too-many-lines,no-member,inconsistent-return-statements
# pylint: disable=too-many-statements,too-few-public-methods

"""
Helper class for formatting error responses with nested details.

The Azure SDK ODataV4Format.message_details() concatenates error detail entries
without newline separators between them, causing all details to run together on
single lines. This helper intercepts the HttpResponseError raised by the LRO
poller and rebuilds the error message with proper formatting.
"""

from knack.util import CLIError


class ErrorFormat:
"""Helper class for formatting error responses with nested details"""

@staticmethod
def _output(parent_cmd, *args, **kwargs): # pylint: disable=unused-argument
"""Custom response transform that prevents nested error details from being duplicated"""
result = parent_cmd.deserialize_output(
parent_cmd.ctx.vars.instance, client_flatten=True
)

if result and isinstance(result, dict) and "error" in result:
error = result.get("error")
if error and isinstance(error, dict):
if "details" in error and isinstance(error["details"], list):
for detail in error["details"]:
if isinstance(detail, dict) and "details" in detail:
del detail["details"]

return result

@staticmethod
def format_error_message(http_error):
"""Build a properly formatted error message with newlines between detail entries.

Reproduces the ODataV4Format layout but fixes the missing newline between
each detail block so they no longer run together on a single line.
"""
error = getattr(http_error, "error", None)
if not error:
return str(http_error)

code = getattr(error, "code", None) or "Unknown"
message = getattr(error, "message", None) or str(http_error)

lines = [
f"({code}) {message}",
f"Code: {code}",
f"Message: {message}",
]

target = getattr(error, "target", None)
if target:
lines.append(f"Target: {target}")

details = getattr(error, "details", None)
if details:
lines.append("Exception Details:")
for i, detail in enumerate(details):
if i > 0:
lines.append("")
d_code = getattr(detail, "code", None) or "Unknown"
d_msg = getattr(detail, "message", None) or ""
lines.append(f"\t({d_code}) {d_msg}")
lines.append(f"\tCode: {d_code}")
lines.append(f"\tMessage: {d_msg}")
d_target = getattr(detail, "target", None)
if d_target:
lines.append(f"\tTarget: {d_target}")

return "\n".join(lines)

@staticmethod
def handle_lro_error(http_error):
"""Raise a CLIError with a properly formatted error message."""
raise CLIError(ErrorFormat.format_error_message(http_error)) from http_error
Loading
Loading