From fdb30b00a11e5abf59015d74d8670984825d1de0 Mon Sep 17 00:00:00 2001 From: Sankalp Kotewar Date: Wed, 12 Feb 2025 08:59:12 +0530 Subject: [PATCH 01/24] initial commit --- .../azure/cli/core/profiles/_shared.py | 4 +- .../disconnectedoperations/__init__.py | 35 ++ .../disconnectedoperations/_client_factory.py | 21 + .../disconnectedoperations/_help.py | 73 ++++ .../disconnectedoperations/_params.py | 30 ++ .../disconnectedoperations/aaz/__init__.py | 6 + .../aaz/latest/__init__.py | 10 + .../aaz/latest/edge/__cmd_group.py | 24 ++ .../aaz/latest/edge/__init__.py | 11 + .../disconnected_operation/__cmd_group.py | 24 ++ .../edge/disconnected_operation/__init__.py | 12 + .../edge/disconnected_operation/_list.py | 396 ++++++++++++++++++ .../image/__cmd_group.py | 24 ++ .../disconnected_operation/image/__init__.py | 12 + .../image/_list_download_uri.py | 221 ++++++++++ .../disconnectedoperations/commands.py | 27 ++ .../disconnectedoperations/custom.py | 345 +++++++++++++++ .../disconnectedoperations/tests/__init__.py | 6 + .../tests/latest/__init__.py | 6 + .../latest/test_disconnectedoperations.py | 24 ++ 20 files changed, 1310 insertions(+), 1 deletion(-) create mode 100644 src/azure-cli/azure/cli/command_modules/disconnectedoperations/__init__.py create mode 100644 src/azure-cli/azure/cli/command_modules/disconnectedoperations/_client_factory.py create mode 100644 src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py create mode 100644 src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py create mode 100644 src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/__init__.py create mode 100644 src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/__init__.py create mode 100644 src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/__cmd_group.py create mode 100644 src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/__init__.py create mode 100644 src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/__cmd_group.py create mode 100644 src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/__init__.py create mode 100644 src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/_list.py create mode 100644 src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/image/__cmd_group.py create mode 100644 src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/image/__init__.py create mode 100644 src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/image/_list_download_uri.py create mode 100644 src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py create mode 100644 src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py create mode 100644 src/azure-cli/azure/cli/command_modules/disconnectedoperations/tests/__init__.py create mode 100644 src/azure-cli/azure/cli/command_modules/disconnectedoperations/tests/latest/__init__.py create mode 100644 src/azure-cli/azure/cli/command_modules/disconnectedoperations/tests/latest/test_disconnectedoperations.py diff --git a/src/azure-cli-core/azure/cli/core/profiles/_shared.py b/src/azure-cli-core/azure/cli/core/profiles/_shared.py index 1c72649519b..44e161f1b2d 100644 --- a/src/azure-cli-core/azure/cli/core/profiles/_shared.py +++ b/src/azure-cli-core/azure/cli/core/profiles/_shared.py @@ -10,7 +10,6 @@ from knack.log import get_logger - logger = get_logger(__name__) @@ -84,6 +83,8 @@ class ResourceType(Enum): # pylint: disable=too-few-public-methods MGMT_CUSTOMLOCATION = ('azure.mgmt.extendedlocation', 'CustomLocations') MGMT_CONTAINERSERVICE = ('azure.mgmt.containerservice', 'ContainerServiceClient') MGMT_APPCONTAINERS = ('azure.mgmt.appcontainers', 'ContainerAppsAPIClient') + MGMT_DISCONNECTEDOPERATIONS = ('azure.mgmt.disconnectedoperations', 'DisconnectedOperationsClient') + # the "None" below will stay till a command module fills in the type so "get_mgmt_service_client" # can be provided with "ResourceType.XXX" to initialize the client object. This usually happens @@ -155,6 +156,7 @@ def default_api_version(self): AZURE_API_PROFILES = { 'latest': { + ResourceType.MGMT_DISCONNECTEDOPERATIONS: '2024-12-01-preview', ResourceType.MGMT_STORAGE: '2024-01-01', ResourceType.MGMT_NETWORK: '2022-01-01', ResourceType.MGMT_COMPUTE: SDKProfile('2024-07-01', { diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/__init__.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/__init__.py new file mode 100644 index 00000000000..1aaa566929a --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/__init__.py @@ -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. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- + +from azure.cli.core import AzCommandsLoader +from azure.cli.command_modules.disconnectedoperations._help import helps # pylint: disable=unused-import +from azure.cli.command_modules.disconnectedoperations._client_factory import cf_image + + +class DisconnectedoperationsCommandsLoader(AzCommandsLoader): + + def __init__(self, cli_ctx=None): + from azure.cli.core.commands import CliCommandType + from azure.cli.core.profiles import ResourceType # required when using python sdk + disconnectedoperations_custom = CliCommandType( + operations_tmpl='azure.cli.command_modules.disconnectedoperations.custom#{}', + client_factory=cf_image) + super(DisconnectedoperationsCommandsLoader, self).__init__(cli_ctx=cli_ctx, + resource_type=ResourceType.MGMT_DISCONNECTEDOPERATIONS, # required when using python sdk + custom_command_type=disconnectedoperations_custom) + + def load_command_table(self, args): + from azure.cli.command_modules.disconnectedoperations.commands import load_command_table + load_command_table(self, args) + return self.command_table + + def load_arguments(self, command): + from azure.cli.command_modules.disconnectedoperations._params import load_arguments + load_arguments(self, command) + + +COMMAND_LOADER_CLS = DisconnectedoperationsCommandsLoader diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_client_factory.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_client_factory.py new file mode 100644 index 00000000000..20df0404d3b --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_client_factory.py @@ -0,0 +1,21 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + + +def get_disconnectedoperations_management_client(cli_ctx, *_): + from azure.cli.core.commands.client_factory import get_mgmt_service_client + from azure.mgmt.disconnectedoperations import DisconnectedOperationsClient + return get_mgmt_service_client(cli_ctx, DisconnectedOperationsClient) + + +def cf_image(cli_ctx, *_): + return get_disconnectedoperations_management_client(cli_ctx).image + +def cf_logos(cli_ctx, *_): + return get_disconnectedoperations_management_client(cli_ctx).logos + +def cf_metadata(cli_ctx, *_): + return get_disconnectedoperations_management_client(cli_ctx).metadata + diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py new file mode 100644 index 00000000000..492066f4645 --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py @@ -0,0 +1,73 @@ +from knack.help_files import helps # pylint: disable=unused-import + +helps['disconnectedoperations'] = """ + type: group + short-summary: Commands to manage disconnected operations. + long-summary: Manage Azure Edge marketplace operations in disconnected environments. +""" + +helps['disconnectedoperations edgemarketplace'] = """ + type: group + short-summary: Manage Edge Marketplace operations. + long-summary: Commands to manage Edge Marketplace images and offers. +""" + +helps['disconnectedoperations edgemarketplace listoffers'] = """ + type: command + short-summary: List available marketplace offers. + long-summary: List all available marketplace offers with their SKUs and versions. + parameters: + - name: --resource-group -g + type: string + required: true + short-summary: Name of resource group. + - name: --management-endpoint + type: string + short-summary: Management endpoint URL. + default: brazilus.management.azure.com + - name: --provider-namespace + type: string + short-summary: Provider namespace. + default: Private.EdgeInternal + - name: --api-version + type: string + short-summary: API version to use. + default: 2023-08-01-preview + examples: + - name: List offers in default format + text: az disconnectedoperations edgemarketplace listoffers -g myResourceGroup + - name: List offers in table format + text: az disconnectedoperations edgemarketplace listoffers -g myResourceGroup --output table + - name: List offers with custom endpoint + text: az disconnectedoperations edgemarketplace listoffers -g myResourceGroup --management-endpoint customendpoint.azure.com +""" + +helps['disconnectedoperations edgemarketplace packageimage'] = """ + type: command + short-summary: Package a marketplace image. + long-summary: Download and package a marketplace image for use in disconnected environments. + parameters: + - name: --resource-group -g + type: string + required: true + short-summary: Name of resource group. + - name: --publisher + type: string + required: true + short-summary: Publisher of the marketplace image. + - name: --offer + type: string + required: true + short-summary: Offer name of the marketplace image. + - name: --sku + type: string + required: true + short-summary: SKU of the marketplace image. + - name: --location -l + type: string + required: true + short-summary: Location for the packaged image. + examples: + - name: Package a Windows Server image + text: az disconnectedoperations edgemarketplace packageimage -g myResourceGroup --publisher MicrosoftWindowsServer --offer WindowsServer --sku 2019-Datacenter --location eastus +""" \ No newline at end of file diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py new file mode 100644 index 00000000000..ec6554388c5 --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py @@ -0,0 +1,30 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- + +# pylint: disable=too-many-lines +# pylint: disable=too-many-statements + +from azure.cli.core.commands.parameters import resource_group_name_type + + +def load_arguments(self, _): # pylint: disable=unused-argument + with self.argument_context('disconnectedoperations edgemarketplace packageimage') as c: + c.argument('resource_group_name', arg_type=resource_group_name_type) + c.argument('publisher', options_list=['--publisher']) + c.argument('offer', options_list=['--offer']) + c.argument('sku', options_list=['--skus']) + c.argument('location', options_list=['--location']) + + with self.argument_context('disconnectedoperations edgemarketplace listoffers') as c: + c.argument('management_endpoint', type=str, + help='Management endpoint URL') + c.argument('provider_namespace', type=str, + help='Provider namespace') + c.argument('sub_provider', type=str, + help='Sub-provider namespace') + c.argument('api_version', type=str, + help='API version') diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/__init__.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/__init__.py new file mode 100644 index 00000000000..5757aea3175 --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/__init__.py @@ -0,0 +1,6 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/__init__.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/__init__.py new file mode 100644 index 00000000000..f6acc11aa4e --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/__init__.py @@ -0,0 +1,10 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- + +# pylint: skip-file +# flake8: noqa + diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/__cmd_group.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/__cmd_group.py new file mode 100644 index 00000000000..30f0e46625f --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/__cmd_group.py @@ -0,0 +1,24 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- + +# pylint: skip-file +# flake8: noqa + +from azure.cli.core.aaz import * + + +@register_command_group( + "edge", + is_preview=True, +) +class __CMDGroup(AAZCommandGroup): + """Edge disconnected operations CLI + """ + pass + + +__all__ = ["__CMDGroup"] diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/__init__.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/__init__.py new file mode 100644 index 00000000000..5a9d61963d6 --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/__init__.py @@ -0,0 +1,11 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- + +# pylint: skip-file +# flake8: noqa + +from .__cmd_group import * diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/__cmd_group.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/__cmd_group.py new file mode 100644 index 00000000000..fce13eff9a7 --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/__cmd_group.py @@ -0,0 +1,24 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- + +# pylint: skip-file +# flake8: noqa + +from azure.cli.core.aaz import * + + +@register_command_group( + "edge disconnected-operation", + is_preview=True, +) +class __CMDGroup(AAZCommandGroup): + """Disconnected operations cli + """ + pass + + +__all__ = ["__CMDGroup"] diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/__init__.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/__init__.py new file mode 100644 index 00000000000..d63ae5a6fc9 --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/__init__.py @@ -0,0 +1,12 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- + +# pylint: skip-file +# flake8: noqa + +from .__cmd_group import * +from ._list import * diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/_list.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/_list.py new file mode 100644 index 00000000000..45101687c88 --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/_list.py @@ -0,0 +1,396 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- + +# pylint: skip-file +# flake8: noqa + +from azure.cli.core.aaz import * + + +@register_command( + "edge disconnected-operation list", + is_preview=True, +) +class List(AAZCommand): + """List DisconnectedOperation resources + + List DisconnectedOperation resources by subscription ID and resource group + """ + + _aaz_info = { + "version": "2024-12-01-preview", + "resources": [ + ["mgmt-plane", "/subscriptions/{}/providers/microsoft.edge/disconnectedoperations", "2024-12-01-preview"], + ["mgmt-plane", "/subscriptions/{}/resourcegroups/{}/providers/microsoft.edge/disconnectedoperations", "2024-12-01-preview"], + ] + } + + AZ_SUPPORT_PAGINATION = True + + def _handler(self, command_args): + super()._handler(command_args) + return self.build_paging(self._execute_operations, self._output) + + _args_schema = None + + @classmethod + def _build_arguments_schema(cls, *args, **kwargs): + if cls._args_schema is not None: + return cls._args_schema + cls._args_schema = super()._build_arguments_schema(*args, **kwargs) + + # define Arg Group "" + + _args_schema = cls._args_schema + _args_schema.resource_group = AAZResourceGroupNameArg() + return cls._args_schema + + def _execute_operations(self): + self.pre_operations() + condition_0 = has_value(self.ctx.subscription_id) and has_value(self.ctx.args.resource_group) is not True + condition_1 = has_value(self.ctx.args.resource_group) and has_value(self.ctx.subscription_id) + if condition_0: + self.DisconnectedOperationsListBySubscription(ctx=self.ctx)() + if condition_1: + self.DisconnectedOperationsListByResourceGroup(ctx=self.ctx)() + self.post_operations() + + @register_callback + def pre_operations(self): + pass + + @register_callback + def post_operations(self): + pass + + def _output(self, *args, **kwargs): + result = self.deserialize_output(self.ctx.vars.instance.value, client_flatten=True) + next_link = self.deserialize_output(self.ctx.vars.instance.next_link) + return result, next_link + + class DisconnectedOperationsListBySubscription(AAZHttpOperation): + CLIENT_TYPE = "MgmtClient" + + def __call__(self, *args, **kwargs): + request = self.make_request() + session = self.client.send_request(request=request, stream=False, **kwargs) + if session.http_response.status_code in [200]: + return self.on_200(session) + + return self.on_error(session.http_response) + + @property + def url(self): + return self.client.format_url( + "/subscriptions/{subscriptionId}/providers/Microsoft.Edge/disconnectedOperations", + **self.url_parameters + ) + + @property + def method(self): + return "GET" + + @property + def error_format(self): + return "MgmtErrorFormat" + + @property + def url_parameters(self): + parameters = { + **self.serialize_url_param( + "subscriptionId", self.ctx.subscription_id, + required=True, + ), + } + return parameters + + @property + def query_parameters(self): + parameters = { + **self.serialize_query_param( + "api-version", "2024-12-01-preview", + required=True, + ), + } + return parameters + + @property + def header_parameters(self): + parameters = { + **self.serialize_header_param( + "Accept", "application/json", + ), + } + return parameters + + def on_200(self, session): + data = self.deserialize_http_content(session) + self.ctx.set_var( + "instance", + data, + schema_builder=self._build_schema_on_200 + ) + + _schema_on_200 = None + + @classmethod + def _build_schema_on_200(cls): + if cls._schema_on_200 is not None: + return cls._schema_on_200 + + cls._schema_on_200 = AAZObjectType() + + _schema_on_200 = cls._schema_on_200 + _schema_on_200.next_link = AAZStrType( + serialized_name="nextLink", + ) + _schema_on_200.value = AAZListType( + flags={"required": True}, + ) + + value = cls._schema_on_200.value + value.Element = AAZObjectType() + + _element = cls._schema_on_200.value.Element + _element.id = AAZStrType( + flags={"read_only": True}, + ) + _element.location = AAZStrType( + flags={"required": True}, + ) + _element.name = AAZStrType( + flags={"read_only": True}, + ) + _element.properties = AAZObjectType() + _element.system_data = AAZObjectType( + serialized_name="systemData", + flags={"read_only": True}, + ) + _element.tags = AAZDictType() + _element.type = AAZStrType( + flags={"read_only": True}, + ) + + properties = cls._schema_on_200.value.Element.properties + properties.billing_model = AAZStrType( + serialized_name="billingModel", + flags={"read_only": True}, + ) + properties.connection_intent = AAZStrType( + serialized_name="connectionIntent", + flags={"required": True}, + ) + properties.connection_status = AAZStrType( + serialized_name="connectionStatus", + flags={"read_only": True}, + ) + properties.device_version = AAZStrType( + serialized_name="deviceVersion", + ) + properties.provisioning_state = AAZStrType( + serialized_name="provisioningState", + flags={"read_only": True}, + ) + properties.registration_status = AAZStrType( + serialized_name="registrationStatus", + ) + properties.stamp_id = AAZStrType( + serialized_name="stampId", + flags={"read_only": True}, + ) + + system_data = cls._schema_on_200.value.Element.system_data + system_data.created_at = AAZStrType( + serialized_name="createdAt", + ) + system_data.created_by = AAZStrType( + serialized_name="createdBy", + ) + system_data.created_by_type = AAZStrType( + serialized_name="createdByType", + ) + system_data.last_modified_at = AAZStrType( + serialized_name="lastModifiedAt", + ) + system_data.last_modified_by = AAZStrType( + serialized_name="lastModifiedBy", + ) + system_data.last_modified_by_type = AAZStrType( + serialized_name="lastModifiedByType", + ) + + tags = cls._schema_on_200.value.Element.tags + tags.Element = AAZStrType() + + return cls._schema_on_200 + + class DisconnectedOperationsListByResourceGroup(AAZHttpOperation): + CLIENT_TYPE = "MgmtClient" + + def __call__(self, *args, **kwargs): + request = self.make_request() + session = self.client.send_request(request=request, stream=False, **kwargs) + if session.http_response.status_code in [200]: + return self.on_200(session) + + return self.on_error(session.http_response) + + @property + def url(self): + return self.client.format_url( + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Edge/disconnectedOperations", + **self.url_parameters + ) + + @property + def method(self): + return "GET" + + @property + def error_format(self): + return "MgmtErrorFormat" + + @property + def url_parameters(self): + parameters = { + **self.serialize_url_param( + "resourceGroupName", self.ctx.args.resource_group, + required=True, + ), + **self.serialize_url_param( + "subscriptionId", self.ctx.subscription_id, + required=True, + ), + } + return parameters + + @property + def query_parameters(self): + parameters = { + **self.serialize_query_param( + "api-version", "2024-12-01-preview", + required=True, + ), + } + return parameters + + @property + def header_parameters(self): + parameters = { + **self.serialize_header_param( + "Accept", "application/json", + ), + } + return parameters + + def on_200(self, session): + data = self.deserialize_http_content(session) + self.ctx.set_var( + "instance", + data, + schema_builder=self._build_schema_on_200 + ) + + _schema_on_200 = None + + @classmethod + def _build_schema_on_200(cls): + if cls._schema_on_200 is not None: + return cls._schema_on_200 + + cls._schema_on_200 = AAZObjectType() + + _schema_on_200 = cls._schema_on_200 + _schema_on_200.next_link = AAZStrType( + serialized_name="nextLink", + ) + _schema_on_200.value = AAZListType( + flags={"required": True}, + ) + + value = cls._schema_on_200.value + value.Element = AAZObjectType() + + _element = cls._schema_on_200.value.Element + _element.id = AAZStrType( + flags={"read_only": True}, + ) + _element.location = AAZStrType( + flags={"required": True}, + ) + _element.name = AAZStrType( + flags={"read_only": True}, + ) + _element.properties = AAZObjectType() + _element.system_data = AAZObjectType( + serialized_name="systemData", + flags={"read_only": True}, + ) + _element.tags = AAZDictType() + _element.type = AAZStrType( + flags={"read_only": True}, + ) + + properties = cls._schema_on_200.value.Element.properties + properties.billing_model = AAZStrType( + serialized_name="billingModel", + flags={"read_only": True}, + ) + properties.connection_intent = AAZStrType( + serialized_name="connectionIntent", + flags={"required": True}, + ) + properties.connection_status = AAZStrType( + serialized_name="connectionStatus", + flags={"read_only": True}, + ) + properties.device_version = AAZStrType( + serialized_name="deviceVersion", + ) + properties.provisioning_state = AAZStrType( + serialized_name="provisioningState", + flags={"read_only": True}, + ) + properties.registration_status = AAZStrType( + serialized_name="registrationStatus", + ) + properties.stamp_id = AAZStrType( + serialized_name="stampId", + flags={"read_only": True}, + ) + + system_data = cls._schema_on_200.value.Element.system_data + system_data.created_at = AAZStrType( + serialized_name="createdAt", + ) + system_data.created_by = AAZStrType( + serialized_name="createdBy", + ) + system_data.created_by_type = AAZStrType( + serialized_name="createdByType", + ) + system_data.last_modified_at = AAZStrType( + serialized_name="lastModifiedAt", + ) + system_data.last_modified_by = AAZStrType( + serialized_name="lastModifiedBy", + ) + system_data.last_modified_by_type = AAZStrType( + serialized_name="lastModifiedByType", + ) + + tags = cls._schema_on_200.value.Element.tags + tags.Element = AAZStrType() + + return cls._schema_on_200 + + +class _ListHelper: + """Helper class for List""" + + +__all__ = ["List"] diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/image/__cmd_group.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/image/__cmd_group.py new file mode 100644 index 00000000000..79a62e83275 --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/image/__cmd_group.py @@ -0,0 +1,24 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- + +# pylint: skip-file +# flake8: noqa + +from azure.cli.core.aaz import * + + +@register_command_group( + "edge disconnected-operation image", + is_preview=True, +) +class __CMDGroup(AAZCommandGroup): + """Disconnected operations image CLI + """ + pass + + +__all__ = ["__CMDGroup"] diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/image/__init__.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/image/__init__.py new file mode 100644 index 00000000000..5e75ed17830 --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/image/__init__.py @@ -0,0 +1,12 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- + +# pylint: skip-file +# flake8: noqa + +from .__cmd_group import * +from ._list_download_uri import * diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/image/_list_download_uri.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/image/_list_download_uri.py new file mode 100644 index 00000000000..2629febe4e2 --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/image/_list_download_uri.py @@ -0,0 +1,221 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- + +# pylint: skip-file +# flake8: noqa + +from azure.cli.core.aaz import * + + +@register_command( + "edge disconnected-operation image list-download-uri", + is_preview=True, +) +class ListDownloadUri(AAZCommand): + """Get deployment manifest. + """ + + _aaz_info = { + "version": "2024-12-01-preview", + "resources": [ + ["mgmt-plane", "/subscriptions/{}/resourcegroups/{}/providers/microsoft.edge/disconnectedoperations/{}/images/{}/listdownloaduri", "2024-12-01-preview"], + ] + } + + def _handler(self, command_args): + super()._handler(command_args) + self._execute_operations() + return self._output() + + _args_schema = None + + @classmethod + def _build_arguments_schema(cls, *args, **kwargs): + if cls._args_schema is not None: + return cls._args_schema + cls._args_schema = super()._build_arguments_schema(*args, **kwargs) + + # define Arg Group "" + + _args_schema = cls._args_schema + _args_schema.image_name = AAZStrArg( + options=["--image-name"], + help="The name of the Image", + required=True, + id_part="child_name_1", + fmt=AAZStrArgFormat( + pattern="^[a-zA-Z0-9-]{3,24}$", + ), + ) + _args_schema.name = AAZStrArg( + options=["--name"], + help="Name of the resource", + required=True, + id_part="name", + fmt=AAZStrArgFormat( + pattern="^[a-zA-Z0-9][a-zA-Z0-9-_]{2,22}[a-zA-Z0-9]$", + ), + ) + _args_schema.resource_group = AAZResourceGroupNameArg( + required=True, + ) + return cls._args_schema + + def _execute_operations(self): + self.pre_operations() + self.ImagesListDownloadUri(ctx=self.ctx)() + self.post_operations() + + @register_callback + def pre_operations(self): + pass + + @register_callback + def post_operations(self): + pass + + def _output(self, *args, **kwargs): + result = self.deserialize_output(self.ctx.vars.instance, client_flatten=True) + return result + + class ImagesListDownloadUri(AAZHttpOperation): + CLIENT_TYPE = "MgmtClient" + + def __call__(self, *args, **kwargs): + request = self.make_request() + session = self.client.send_request(request=request, stream=False, **kwargs) + if session.http_response.status_code in [200]: + return self.on_200(session) + + return self.on_error(session.http_response) + + @property + def url(self): + return self.client.format_url( + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Edge/disconnectedOperations/{name}/images/{imageName}/listDownloadUri", + **self.url_parameters + ) + + @property + def method(self): + return "POST" + + @property + def error_format(self): + return "MgmtErrorFormat" + + @property + def url_parameters(self): + parameters = { + **self.serialize_url_param( + "imageName", self.ctx.args.image_name, + required=True, + ), + **self.serialize_url_param( + "name", self.ctx.args.name, + required=True, + ), + **self.serialize_url_param( + "resourceGroupName", self.ctx.args.resource_group, + required=True, + ), + **self.serialize_url_param( + "subscriptionId", self.ctx.subscription_id, + required=True, + ), + } + return parameters + + @property + def query_parameters(self): + parameters = { + **self.serialize_query_param( + "api-version", "2024-12-01-preview", + required=True, + ), + } + return parameters + + @property + def header_parameters(self): + parameters = { + **self.serialize_header_param( + "Accept", "application/json", + ), + } + return parameters + + def on_200(self, session): + data = self.deserialize_http_content(session) + self.ctx.set_var( + "instance", + data, + schema_builder=self._build_schema_on_200 + ) + + _schema_on_200 = None + + @classmethod + def _build_schema_on_200(cls): + if cls._schema_on_200 is not None: + return cls._schema_on_200 + + cls._schema_on_200 = AAZObjectType() + + _schema_on_200 = cls._schema_on_200 + _schema_on_200.compatible_versions = AAZListType( + serialized_name="compatibleVersions", + flags={"read_only": True}, + ) + _schema_on_200.download_link = AAZStrType( + serialized_name="downloadLink", + flags={"read_only": True}, + ) + _schema_on_200.link_expiry = AAZStrType( + serialized_name="linkExpiry", + flags={"read_only": True}, + ) + _schema_on_200.provisioning_state = AAZStrType( + serialized_name="provisioningState", + flags={"read_only": True}, + ) + _schema_on_200.release_date = AAZStrType( + serialized_name="releaseDate", + flags={"read_only": True}, + ) + _schema_on_200.release_display_name = AAZStrType( + serialized_name="releaseDisplayName", + flags={"read_only": True}, + ) + _schema_on_200.release_notes = AAZStrType( + serialized_name="releaseNotes", + flags={"read_only": True}, + ) + _schema_on_200.release_type = AAZStrType( + serialized_name="releaseType", + flags={"read_only": True}, + ) + _schema_on_200.release_version = AAZStrType( + serialized_name="releaseVersion", + flags={"read_only": True}, + ) + _schema_on_200.transaction_id = AAZStrType( + serialized_name="transactionId", + flags={"read_only": True}, + ) + + compatible_versions = cls._schema_on_200.compatible_versions + compatible_versions.Element = AAZStrType() + + return cls._schema_on_200 + + +class _ListDownloadUriHelper: + """Helper class for ListDownloadUri""" + + +__all__ = ["ListDownloadUri"] diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py new file mode 100644 index 00000000000..cc842ac3e1b --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py @@ -0,0 +1,27 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- + +# pylint: disable=too-many-lines +# pylint: disable=too-many-statements + +# from azure.cli.core.commands import CliCommandType +# from azure.cli.core.profiles import ResourceType + +from azure.cli.core.commands import CliCommandType + +def load_command_table(self, _): + custom_command_type = CliCommandType( + operations_tmpl='azure.cli.command_modules.disconnectedoperations.custom#{}' + ) + + with self.command_group('disconnectedoperations edgemarketplace', custom_command_type=custom_command_type) as g: + g.custom_command('packageimage', 'package_image') + g.custom_command('listoffers', 'list_offers') + g.custom_command('get-download-url', 'get_image_download_url') + g.custom_command('getoffer', 'get_offer') + + return self.command_table \ No newline at end of file diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py new file mode 100644 index 00000000000..77dd8b3b112 --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py @@ -0,0 +1,345 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- + +# pylint: disable=too-many-lines +# pylint: disable=too-many-statements + +from azure.cli.core import AzCommandsLoader +from knack.log import get_logger + + +logger = get_logger(__name__) + + + +def get_offer(cmd, + resource_group_name, + offer_name, + output_folder=None, + management_endpoint="brazilus.management.azure.com", + provider_namespace="Private.EdgeInternal", + sub_provider="Microsoft.EdgeMarketPlace", + api_version="2023-08-01-preview"): + """ + Get details of a specific marketplace offer and download its logos. + + Args: + cmd: The command context object + resource_group_name: Name of resource group + offer_name: Name of the offer to retrieve + output_folder: Folder path to save logos (optional) + management_endpoint: Management endpoint URL + provider_namespace: Provider namespace + sub_provider: Sub-provider namespace + api_version: API version + """ + import os + import json + import requests + from azure.cli.core.commands.client_factory import get_subscription_id + from azure.cli.core.util import send_raw_request + from knack.log import get_logger + + logger = get_logger(__name__) + + # Get subscription ID from current context + subscription_id = get_subscription_id(cmd.cli_ctx) + + # Construct URL with parameters + url = ( + f"https://{management_endpoint}" + f"/subscriptions/{subscription_id}" + f"/resourceGroups/{resource_group_name}" + f"/providers/{provider_namespace}/disconnectedOperations/demo-winfield1" + f"/providers/{sub_provider}/offers/{offer_name}" + f"?api-version={api_version}" + ) + + resource = "https://management.azure.com" + + try: + response = send_raw_request(cmd.cli_ctx, 'get', url, resource=resource) + + if response.status_code == 200: + data = response.json() + offer_content = data.get('properties', {}).get('offerContent', {}) + icon_uris = offer_content.get('iconFileUris', {}) + + # Download logos and metadata if output folder is specified + if output_folder: + publisher_id = offer_content.get('offerPublisher', {}).get('publisherId', '') + offer_id = offer_content.get('offerId', '') + skus = data.get('properties', {}).get('marketplaceSkus', []) + + for sku in skus: + sku_id = sku.get('marketplaceSkuId', '') + versions = sku.get('marketplaceSkuVersions', []) + + for version in versions: + version_id = version.get('name') + + # Create base path for this version + base_path = os.path.join(output_folder, 'catalog_artifacts', + publisher_id, offer_id, sku_id, version_id) + icon_path = os.path.join(base_path, 'icons') + os.makedirs(icon_path, exist_ok=True) + + # Save metadata.json + metadata_path = os.path.join(base_path, 'metadata.json') + metadata = { + 'name': data.get('name'), + 'publisher': offer_content.get('offerPublisher'), + 'offer_id': offer_content.get('offerId'), + 'summary': offer_content.get('summary'), + 'description': offer_content.get('description'), + 'sku': { + 'name': sku.get('displayName'), + 'id': sku.get('marketplaceSkuId'), + 'os_type': sku.get('operatingSystem'), + 'version': version + } + } + + with open(metadata_path, 'w', encoding='utf-8') as f: + json.dump(metadata, f, indent=2) + logger.info(f"Saved metadata to {metadata_path}") + + # Download icons + if icon_uris: + for size, uri in icon_uris.items(): + try: + logo_response = requests.get(uri) + if logo_response.status_code == 200: + file_extension = 'png' + file_path = os.path.join(icon_path, f"{size}.{file_extension}") + + with open(file_path, 'wb') as f: + f.write(logo_response.content) + logger.info(f"Downloaded {size} logo to {file_path}") + else: + logger.error(f"Failed to download {size} logo: {logo_response.status_code}") + except Exception as e: + logger.error(f"Error downloading {size} logo: {str(e)}") + + + # Format offer details + result = { + 'name': data.get('name'), + 'publisher': offer_content.get('offerPublisher', {}).get('publisherDisplayName'), + 'offer_id': offer_content.get('offerId'), + 'summary': offer_content.get('summary'), + 'description': offer_content.get('description'), + 'skus': [] + } + + # Add SKU information + skus = data.get('properties', {}).get('marketplaceSkus', []) + for sku in skus: + sku_info = { + 'name': sku.get('displayName'), + 'id': sku.get('marketplaceSkuId'), + 'os_type': sku.get('operatingSystem', {}).get('type'), + 'versions': [ + { + 'version': v.get('name'), + 'size_mb': v.get('minimumDownloadSizeInMb') + } for v in sku.get('marketplaceSkuVersions', [])[:3] # Latest 3 versions + ] + } + result['skus'].append(sku_info) + + return result + + else: + error_message = f"Request failed with status code: {response.status_code}" + logger.error(error_message) + return { + 'error': error_message, + 'status': 'failed', + 'resource_group_name': resource_group_name, + 'response': response.text + } + + except Exception as e: + logger.error(f"Failed to retrieve offer: {str(e)}") + return { + 'error': str(e), + 'status': 'failed', + 'resource_group_name': resource_group_name + } + +def get_image_download_url(cmd, + resource_group_name, + publisher, + offer, + sku, + version, + management_endpoint="brazilus.management.azure.com", + provider_namespace="Private.EdgeInternal", + api_version="2024-11-01-preview"): + """ + Get download URL for a specific marketplace image version. + """ + from azure.cli.core.commands.client_factory import get_subscription_id + from azure.cli.core.util import send_raw_request + from knack.log import get_logger + + logger = get_logger(__name__) + + # Get subscription ID + subscription_id = get_subscription_id(cmd.cli_ctx) + + # Construct URL for the listDownloadUri API + url = ( + f"https://{management_endpoint}" + f"/subscriptions/{subscription_id}" + f"/resourceGroups/{resource_group_name}" + f"/providers/{provider_namespace}/disconnectedOperations/demo-winfield1" + f"/images/{publisher}.{offer}.{sku}.{version}/listDownloadUri" + f"?api-version={api_version}" + ) + + try: + # Make POST request to get download URL + response = send_raw_request(cmd.cli_ctx, 'post', url, + resource="https://management.azure.com") + + if response.status_code == 200: + download_info = response.json() + return { + 'download_url': download_info.get('downloadUri'), + 'expiry': download_info.get('expiryTime'), + 'publisher': publisher, + 'offer': offer, + 'sku': sku, + 'version': version + } + else: + error_message = f"Failed to get download URL. Status code: {response.status_code}" + logger.error(error_message) + return { + 'error': error_message, + 'status': 'failed', + 'response': response.text + } + + except Exception as e: + logger.error(f"Error getting download URL: {str(e)}") + return { + 'error': str(e), + 'status': 'failed' + } + +def package_image(cmd, + resource_group_name, + publisher, + offer, + sku, + location): + self.kwargs.update({ + 'resource_group_name': resource_group_name, + 'publisher': publisher, + 'offer': offer, + 'sku': sku + }) + + # download metadata + + + # download the icons + + # download the image + + return { + 'resource_group_name': resource_group_name, + 'publisher': publisher, + 'offer': offer, + 'sku': sku, + 'location': location, + 'status': 'success' + } + +def list_offers(cmd, + resource_group_name, + management_endpoint="brazilus.management.azure.com", + provider_namespace="Private.EdgeInternal", + sub_provider="Microsoft.EdgeMarketPlace", + api_version="2023-08-01-preview"): + """ + List all offers for disconnected operations. + """ + from azure.cli.core.profiles import ResourceType + from azure.cli.core.commands.client_factory import get_subscription_id + from azure.cli.core.util import send_raw_request + from knack.log import get_logger + + logger = get_logger(__name__) + + # Get subscription ID from current context + subscription_id = get_subscription_id(cmd.cli_ctx) + + # Construct URL with parameters + url = ( + f"https://{management_endpoint}" + f"/subscriptions/{subscription_id}" + f"/resourceGroups/{resource_group_name}" + f"/providers/{provider_namespace}/disconnectedOperations/demo-winfield1" + f"/providers/{sub_provider}/offers" + f"?api-version={api_version}" + ) + + # Define headers with resource for authentication + headers = { + 'Content-Type': 'application/json', + } + + # Define the resource for authentication + resource = "https://management.azure.com" # Using standard Azure management endpoint + + try: + response = send_raw_request(cmd.cli_ctx, 'get', url, resource=resource) + + if response.status_code == 200: + data = response.json() + + # Format data for output + result = [] + for offer in data.get('value', []): + offer_content = offer.get('properties', {}).get('offerContent', {}) + skus = offer.get('properties', {}).get('marketplaceSkus', []) + + for sku in skus: + versions = sku.get('marketplaceSkuVersions', []) + for version in versions[:3]: # Show only latest 3 versions + row = { + 'Publisher': offer_content.get('offerPublisher', {}).get('publisherDisplayName'), + 'Offer': offer_content.get('offerId'), + 'SKU': sku.get('marketplaceSkuId'), + 'Version': version.get('name'), + 'OS_Type': sku.get('operatingSystem', {}).get('type'), + 'Size_MB': version.get('minimumDownloadSizeInMb') + } + result.append(row) + + return result + else: + error_message = f"Request failed with status code: {response.status_code}" + logger.error(error_message) + return { + 'error': error_message, + 'status': 'failed', + 'resource_group_name': resource_group_name, + 'response': response.text + } + + except Exception as e: + logger.error(f"Failed to retrieve offers: {str(e)}") + return { + 'error': str(e), + 'status': 'failed', + 'resource_group_name': resource_group_name + } \ No newline at end of file diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/tests/__init__.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/tests/__init__.py new file mode 100644 index 00000000000..5757aea3175 --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/tests/__init__.py @@ -0,0 +1,6 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/tests/latest/__init__.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/tests/latest/__init__.py new file mode 100644 index 00000000000..5757aea3175 --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/tests/latest/__init__.py @@ -0,0 +1,6 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/tests/latest/test_disconnectedoperations.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/tests/latest/test_disconnectedoperations.py new file mode 100644 index 00000000000..35610802cc7 --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/tests/latest/test_disconnectedoperations.py @@ -0,0 +1,24 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- + +from azure.cli.testsdk import * + + +class DisconnectedoperationsScenario(ScenarioTest): + @ResourceGroupPreparer(name_prefix='cli_test_mycommand') + def test_my_command(self, resource_group): + + self.kwargs.update({ + 'resource_group_name': resource_group, + 'publisher': 'publisher', + 'offer': 'offer', + 'sku': 'sku' + }) + # Run the command and check the output + result = self.cmd('az disconnectedoperations package') + self.assertEqual(result, 'hello') + \ No newline at end of file From 20ccfa2ae80ba86788382dca2a0e67eb6b2873d4 Mon Sep 17 00:00:00 2001 From: Sankalp Kotewar Date: Wed, 12 Feb 2025 09:24:58 +0530 Subject: [PATCH 02/24] fixed linter issues --- .../disconnectedoperations/_help.py | 89 ++++++++++++++++--- .../disconnectedoperations/_params.py | 54 +++++++---- .../disconnectedoperations/commands.py | 16 ++-- .../disconnectedoperations/custom.py | 80 ++++++----------- 4 files changed, 143 insertions(+), 96 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py index 492066f4645..3692f1572b1 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py @@ -24,28 +24,75 @@ - name: --management-endpoint type: string short-summary: Management endpoint URL. - default: brazilus.management.azure.com + long-summary: Uses brazilus.management.azure.com for test environment, management.azure.com for production. + default: management.azure.com - name: --provider-namespace type: string short-summary: Provider namespace. - default: Private.EdgeInternal + long-summary: Use "Private.EdgeInternal" for test environment or "Microsoft.Edge" for production. + default: Microsoft.Edge + - name: --sub-provider + type: string + short-summary: Sub-provider namespace. + default: Microsoft.EdgeMarketPlace - name: --api-version type: string short-summary: API version to use. default: 2023-08-01-preview examples: - - name: List offers in default format + - name: List offers using production environment text: az disconnectedoperations edgemarketplace listoffers -g myResourceGroup + - name: List offers using test environment + text: az disconnectedoperations edgemarketplace listoffers -g myResourceGroup --provider-namespace Private.EdgeInternal - name: List offers in table format text: az disconnectedoperations edgemarketplace listoffers -g myResourceGroup --output table - - name: List offers with custom endpoint - text: az disconnectedoperations edgemarketplace listoffers -g myResourceGroup --management-endpoint customendpoint.azure.com """ -helps['disconnectedoperations edgemarketplace packageimage'] = """ +helps['disconnectedoperations edgemarketplace get-offer'] = """ type: command - short-summary: Package a marketplace image. - long-summary: Download and package a marketplace image for use in disconnected environments. + short-summary: Get details of a specific marketplace offer. + long-summary: Retrieve detailed information about a marketplace offer and optionally download its logos. + parameters: + - name: --resource-group -g + type: string + required: true + short-summary: Name of resource group. + - name: --offer-name + type: string + required: true + short-summary: Name of the offer to retrieve. + - name: --output-folder + type: string + short-summary: Local folder path to save logos and metadata. + - name: --management-endpoint + type: string + short-summary: Management endpoint URL. + long-summary: Uses brazilus.management.azure.com for test environment, management.azure.com for production. + default: management.azure.com + - name: --provider-namespace + type: string + short-summary: Provider namespace. + long-summary: Use "Private.EdgeInternal" for test environment or "Microsoft.Edge" for production. + default: Microsoft.Edge + - name: --sub-provider + type: string + short-summary: Sub-provider namespace. + default: Microsoft.EdgeMarketPlace + - name: --api-version + type: string + short-summary: API version to use. + default: 2023-08-01-preview + examples: + - name: Get offer details using production environment + text: az disconnectedoperations edgemarketplace get-offer -g myResourceGroup --offer-name myOffer + - name: Get offer details and save logos using test environment + text: az disconnectedoperations edgemarketplace get-offer -g myResourceGroup --offer-name myOffer --output-folder ./artifacts --provider-namespace Private.EdgeInternal +""" + +helps['disconnectedoperations edgemarketplace get-image-download-url'] = """ + type: command + short-summary: Get download URL for a marketplace image. + long-summary: Get the download URL for a specific marketplace image version. parameters: - name: --resource-group -g type: string @@ -62,12 +109,28 @@ - name: --sku type: string required: true - short-summary: SKU of the marketplace image. - - name: --location -l + short-summary: SKU identifier. + - name: --version type: string required: true - short-summary: Location for the packaged image. + short-summary: Version of the marketplace image. + - name: --management-endpoint + type: string + short-summary: Management endpoint URL. + long-summary: Uses brazilus.management.azure.com for test environment, management.azure.com for production. + default: management.azure.com + - name: --provider-namespace + type: string + short-summary: Provider namespace. + long-summary: Use "Private.EdgeInternal" for test environment or "Microsoft.Edge" for production. + default: Microsoft.Edge + - name: --api-version + type: string + short-summary: API version to use. + default: 2024-11-01-preview examples: - - name: Package a Windows Server image - text: az disconnectedoperations edgemarketplace packageimage -g myResourceGroup --publisher MicrosoftWindowsServer --offer WindowsServer --sku 2019-Datacenter --location eastus + - name: Get image download URL using production environment + text: az disconnectedoperations edgemarketplace get-image-download-url -g myResourceGroup --publisher MicrosoftWindowsServer --offer WindowsServer --sku 2019-Datacenter --version latest + - name: Get image download URL using test environment + text: az disconnectedoperations edgemarketplace get-image-download-url -g myResourceGroup --publisher MicrosoftWindowsServer --offer WindowsServer --sku 2019-Datacenter --version latest --provider-namespace Private.EdgeInternal """ \ No newline at end of file diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py index ec6554388c5..23a429126ed 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py @@ -5,26 +5,44 @@ # Code generated by aaz-dev-tools # -------------------------------------------------------------------------------------------- -# pylint: disable=too-many-lines -# pylint: disable=too-many-statements - from azure.cli.core.commands.parameters import resource_group_name_type - +from knack.arguments import CLIArgumentType def load_arguments(self, _): # pylint: disable=unused-argument - with self.argument_context('disconnectedoperations edgemarketplace packageimage') as c: - c.argument('resource_group_name', arg_type=resource_group_name_type) - c.argument('publisher', options_list=['--publisher']) - c.argument('offer', options_list=['--offer']) - c.argument('sku', options_list=['--skus']) - c.argument('location', options_list=['--location']) + provider_namespace_type = CLIArgumentType( + type=str, + help='Provider namespace. Use "Private.EdgeInternal" for test environment or "Microsoft.Edge" for production', + default="Microsoft.Edge" + ) + management_endpoint_type = CLIArgumentType( + type=str, + help='Management endpoint URL. Uses brazilus.management.azure.com for test environment, management.azure.com for production', + default="management.azure.com" + ) + with self.argument_context('disconnectedoperations edgemarketplace listoffers') as c: - c.argument('management_endpoint', type=str, - help='Management endpoint URL') - c.argument('provider_namespace', type=str, - help='Provider namespace') - c.argument('sub_provider', type=str, - help='Sub-provider namespace') - c.argument('api_version', type=str, - help='API version') + c.argument('resource_group_name', arg_type=resource_group_name_type) + c.argument('management_endpoint', arg_type=management_endpoint_type) + c.argument('provider_namespace', arg_type=provider_namespace_type) + c.argument('sub_provider', type=str, help='Sub-provider namespace', default="Microsoft.EdgeMarketPlace") + c.argument('api_version', type=str, help='API version', default="2023-08-01-preview") + + with self.argument_context('disconnectedoperations edgemarketplace get-offer') as c: + c.argument('resource_group_name', arg_type=resource_group_name_type) + c.argument('offer_name', type=str, help='Name of the offer to retrieve') + c.argument('output_folder', type=str, help='Local folder path to save logos and metadata') + c.argument('management_endpoint', arg_type=management_endpoint_type) + c.argument('provider_namespace', arg_type=provider_namespace_type) + c.argument('sub_provider', type=str, help='Sub-provider namespace', default="Microsoft.EdgeMarketPlace") + c.argument('api_version', type=str, help='API version', default="2023-08-01-preview") + + with self.argument_context('disconnectedoperations edgemarketplace get-image-download-url') as c: + c.argument('resource_group_name', arg_type=resource_group_name_type) + c.argument('publisher', type=str, help='Publisher of the marketplace image') + c.argument('offer', type=str, help='Offer name') + c.argument('sku', type=str, help='SKU identifier') + c.argument('version', type=str, help='Version of the marketplace image') + c.argument('management_endpoint', arg_type=management_endpoint_type) + c.argument('provider_namespace', arg_type=provider_namespace_type) + c.argument('api_version', type=str, help='API version', default="2024-11-01-preview") \ No newline at end of file diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py index cc842ac3e1b..3aac4f08719 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py @@ -5,12 +5,6 @@ # Code generated by aaz-dev-tools # -------------------------------------------------------------------------------------------- -# pylint: disable=too-many-lines -# pylint: disable=too-many-statements - -# from azure.cli.core.commands import CliCommandType -# from azure.cli.core.profiles import ResourceType - from azure.cli.core.commands import CliCommandType def load_command_table(self, _): @@ -19,9 +13,11 @@ def load_command_table(self, _): ) with self.command_group('disconnectedoperations edgemarketplace', custom_command_type=custom_command_type) as g: - g.custom_command('packageimage', 'package_image') - g.custom_command('listoffers', 'list_offers') - g.custom_command('get-download-url', 'get_image_download_url') - g.custom_command('getoffer', 'get_offer') + g.custom_command('listoffers', 'list_offers', + help='List all marketplace offers for disconnected operations') + g.custom_command('get-image-download-url', 'get_image_download_url', + help='Get download URL for a specific marketplace image version') + g.custom_command('get-offer', 'get_offer', + help='Get details of a specific marketplace offer and download its logos') return self.command_table \ No newline at end of file diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py index 77dd8b3b112..b8c3f01c09e 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py @@ -14,29 +14,21 @@ logger = get_logger(__name__) +def _get_management_endpoint(provider_namespace): + """Helper function to determine management endpoint based on provider namespace.""" + return "brazilus.management.azure.com" if provider_namespace == "Private.EdgeInternal" else "management.azure.com" def get_offer(cmd, resource_group_name, offer_name, output_folder=None, - management_endpoint="brazilus.management.azure.com", - provider_namespace="Private.EdgeInternal", + management_endpoint=None, + provider_namespace="Microsoft.Edge", sub_provider="Microsoft.EdgeMarketPlace", api_version="2023-08-01-preview"): - """ - Get details of a specific marketplace offer and download its logos. - - Args: - cmd: The command context object - resource_group_name: Name of resource group - offer_name: Name of the offer to retrieve - output_folder: Folder path to save logos (optional) - management_endpoint: Management endpoint URL - provider_namespace: Provider namespace - sub_provider: Sub-provider namespace - api_version: API version - """ + """Get details of a specific marketplace offer and download its logos.""" + import os import json import requests @@ -44,6 +36,9 @@ def get_offer(cmd, from azure.cli.core.util import send_raw_request from knack.log import get_logger + # Use helper function if management_endpoint not explicitly provided + if management_endpoint is None: + management_endpoint = _get_management_endpoint(provider_namespace) logger = get_logger(__name__) # Get subscription ID from current context @@ -178,16 +173,18 @@ def get_image_download_url(cmd, offer, sku, version, - management_endpoint="brazilus.management.azure.com", - provider_namespace="Private.EdgeInternal", + management_endpoint=None, + provider_namespace="Microsoft.Edge", api_version="2024-11-01-preview"): - """ - Get download URL for a specific marketplace image version. - """ + """Get download URL for a specific marketplace image version.""" + from azure.cli.core.commands.client_factory import get_subscription_id from azure.cli.core.util import send_raw_request from knack.log import get_logger - + + if management_endpoint is None: + management_endpoint = _get_management_endpoint(provider_namespace) + logger = get_logger(__name__) # Get subscription ID @@ -234,44 +231,14 @@ def get_image_download_url(cmd, 'status': 'failed' } -def package_image(cmd, - resource_group_name, - publisher, - offer, - sku, - location): - self.kwargs.update({ - 'resource_group_name': resource_group_name, - 'publisher': publisher, - 'offer': offer, - 'sku': sku - }) - - # download metadata - - - # download the icons - - # download the image - - return { - 'resource_group_name': resource_group_name, - 'publisher': publisher, - 'offer': offer, - 'sku': sku, - 'location': location, - 'status': 'success' - } - def list_offers(cmd, resource_group_name, - management_endpoint="brazilus.management.azure.com", - provider_namespace="Private.EdgeInternal", + management_endpoint=None, + provider_namespace="Microsoft.Edge", sub_provider="Microsoft.EdgeMarketPlace", api_version="2023-08-01-preview"): - """ - List all offers for disconnected operations. - """ + """List all offers for disconnected operations.""" + from azure.cli.core.profiles import ResourceType from azure.cli.core.commands.client_factory import get_subscription_id from azure.cli.core.util import send_raw_request @@ -279,6 +246,9 @@ def list_offers(cmd, logger = get_logger(__name__) + if management_endpoint is None: + management_endpoint = _get_management_endpoint(provider_namespace) + # Get subscription ID from current context subscription_id = get_subscription_id(cmd.cli_ctx) From fb715369fc2e94242c89fe32a1fc83fdaa0dda72 Mon Sep 17 00:00:00 2001 From: Sankalp Kotewar Date: Tue, 18 Feb 2025 15:37:49 +0530 Subject: [PATCH 03/24] Added enhanced list and download logic --- .../disconnectedoperations/_params.py | 34 ++- .../disconnectedoperations/commands.py | 41 +++- .../disconnectedoperations/custom.py | 203 ++++++------------ 3 files changed, 118 insertions(+), 160 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py index 23a429126ed..166d6ed135d 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py @@ -11,38 +11,28 @@ def load_arguments(self, _): # pylint: disable=unused-argument provider_namespace_type = CLIArgumentType( type=str, - help='Provider namespace. Use "Private.EdgeInternal" for test environment or "Microsoft.Edge" for production', - default="Microsoft.Edge" + help='Provider namespace. Use "Private.EdgeInternal" for test environment or "Microsoft.EdgeMarketplace" for production', + default="Private.EdgeInternal" ) management_endpoint_type = CLIArgumentType( type=str, - help='Management endpoint URL. Uses brazilus.management.azure.com for test environment, management.azure.com for production', - default="management.azure.com" + help='Management endpoint URL. Use brazilus.management.azure.com for test environment, management.azure.com for production', + default="brazilus.management.azure.com" ) with self.argument_context('disconnectedoperations edgemarketplace listoffers') as c: c.argument('resource_group_name', arg_type=resource_group_name_type) - c.argument('management_endpoint', arg_type=management_endpoint_type) - c.argument('provider_namespace', arg_type=provider_namespace_type) - c.argument('sub_provider', type=str, help='Sub-provider namespace', default="Microsoft.EdgeMarketPlace") - c.argument('api_version', type=str, help='API version', default="2023-08-01-preview") - with self.argument_context('disconnectedoperations edgemarketplace get-offer') as c: + with self.argument_context('disconnectedoperations edgemarketplace getoffer') as c: c.argument('resource_group_name', arg_type=resource_group_name_type) c.argument('offer_name', type=str, help='Name of the offer to retrieve') - c.argument('output_folder', type=str, help='Local folder path to save logos and metadata') - c.argument('management_endpoint', arg_type=management_endpoint_type) - c.argument('provider_namespace', arg_type=provider_namespace_type) - c.argument('sub_provider', type=str, help='Sub-provider namespace', default="Microsoft.EdgeMarketPlace") - c.argument('api_version', type=str, help='API version', default="2023-08-01-preview") + c.argument('product_name', type=str, help='Name of the product to retrieve') - with self.argument_context('disconnectedoperations edgemarketplace get-image-download-url') as c: + with self.argument_context('disconnectedoperations edgemarketplace packageoffer') as c: c.argument('resource_group_name', arg_type=resource_group_name_type) - c.argument('publisher', type=str, help='Publisher of the marketplace image') - c.argument('offer', type=str, help='Offer name') - c.argument('sku', type=str, help='SKU identifier') - c.argument('version', type=str, help='Version of the marketplace image') - c.argument('management_endpoint', arg_type=management_endpoint_type) - c.argument('provider_namespace', arg_type=provider_namespace_type) - c.argument('api_version', type=str, help='API version', default="2024-11-01-preview") \ No newline at end of file + c.argument('publisher_name', type=str, help='Name of the publisher') + c.argument('offer_name', type=str, help='Name of the offer to package') + c.argument('sku', type=str, help='SKU of the product to retrieve') + c.argument('version', type=str, help='Version of the product to retrieve') + c.argument('output_folder', type=str, help='Drive and directory to save the package to. Example: E:\\ or D:\\packages\\') diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py index 3aac4f08719..57bd67d294e 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py @@ -5,7 +5,39 @@ # Code generated by aaz-dev-tools # -------------------------------------------------------------------------------------------- + from azure.cli.core.commands import CliCommandType +from collections import OrderedDict + +def transform_offers_table(result): + if not result: + return result + + # Transform each row while preserving order + transformed = [] + for item in result: + # Format versions to be on separate lines if it's a list/array + versions = item['Versions'] + if isinstance(versions, str): + # Split by comma if it's a comma-separated string + versions = [v.strip() for v in versions.split(',')] + + if isinstance(versions, (list, tuple)): + # Format each version on a new line, preserving the full format + formatted_versions = '\n'.join(str(v).strip() for v in versions) + else: + formatted_versions = str(versions) + + row = OrderedDict([ + ('Publisher', item['Publisher']), + ('Offer', item['Offer']), + ('SKU', item['SKU']), + ('Version', formatted_versions), + ('OS_Type', item['OS_Type']) + ]) + transformed.append(row) + + return transformed def load_command_table(self, _): custom_command_type = CliCommandType( @@ -13,11 +45,8 @@ def load_command_table(self, _): ) with self.command_group('disconnectedoperations edgemarketplace', custom_command_type=custom_command_type) as g: - g.custom_command('listoffers', 'list_offers', - help='List all marketplace offers for disconnected operations') - g.custom_command('get-image-download-url', 'get_image_download_url', - help='Get download URL for a specific marketplace image version') - g.custom_command('get-offer', 'get_offer', - help='Get details of a specific marketplace offer and download its logos') + g.custom_command('listoffers', 'list_offers', table_transformer=transform_offers_table) + g.custom_command('getoffer', 'get_offer') + g.custom_command('packageoffer', 'package_offer') return self.command_table \ No newline at end of file diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py index b8c3f01c09e..408fe8b0978 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py @@ -8,25 +8,22 @@ # pylint: disable=too-many-lines # pylint: disable=too-many-statements -from azure.cli.core import AzCommandsLoader from knack.log import get_logger - logger = get_logger(__name__) -def _get_management_endpoint(provider_namespace): +def _get_management_endpoint(): """Helper function to determine management endpoint based on provider namespace.""" - return "brazilus.management.azure.com" if provider_namespace == "Private.EdgeInternal" else "management.azure.com" + return "brazilus.management.azure.com" # if provider_namespace == "Private.EdgeInternal" else "management.azure.com" -def get_offer(cmd, +def package_offer(cmd, resource_group_name, + publisher_name, offer_name, - output_folder=None, - management_endpoint=None, - provider_namespace="Microsoft.Edge", - sub_provider="Microsoft.EdgeMarketPlace", - api_version="2023-08-01-preview"): + sku, + version, + output_folder): """Get details of a specific marketplace offer and download its logos.""" import os @@ -37,20 +34,23 @@ def get_offer(cmd, from knack.log import get_logger # Use helper function if management_endpoint not explicitly provided - if management_endpoint is None: - management_endpoint = _get_management_endpoint(provider_namespace) + management_endpoint = _get_management_endpoint() logger = get_logger(__name__) # Get subscription ID from current context subscription_id = get_subscription_id(cmd.cli_ctx) + provider_namespace = "Private.EdgeInternal" + sub_provider = "Microsoft.EdgeMarketPlace" + api_version = "2023-08-01-preview" + # Construct URL with parameters url = ( f"https://{management_endpoint}" f"/subscriptions/{subscription_id}" f"/resourceGroups/{resource_group_name}" f"/providers/{provider_namespace}/disconnectedOperations/demo-winfield1" - f"/providers/{sub_provider}/offers/{offer_name}" + f"/providers/{sub_provider}/offers/{publisher_name}:{offer_name}" f"?api-version={api_version}" ) @@ -73,18 +73,44 @@ def get_offer(cmd, for sku in skus: sku_id = sku.get('marketplaceSkuId', '') versions = sku.get('marketplaceSkuVersions', []) + + # If version is specified, filter for that version, else take the latest + if version: + versions = [v for v in versions if v.get('name') == version] + else: + versions = versions[:1] # Take only the latest version + + if not versions: + logger.warning(f"No matching version found for SKU {sku_id}") + continue for version in versions: version_id = version.get('name') # Create base path for this version base_path = os.path.join(output_folder, 'catalog_artifacts', - publisher_id, offer_id, sku_id, version_id) + publisher_id, offer_id, sku_id) + version_level_path = os.path.join(base_path, version_id) icon_path = os.path.join(base_path, 'icons') + + # Check if version directory exists and has content + if os.path.exists(version_level_path): + # Check if directory has any files + if os.path.exists(os.path.join(version_level_path, 'metadata.json')) or \ + any(os.scandir(version_level_path)): + error_message = f"Version directory already exists and contains files: {version_level_path}. Please delete the version folder in case you want to re-download the package." + logger.error(error_message) + return { + 'error': error_message, + 'status': 'failed', + 'path': version_level_path + } + os.makedirs(icon_path, exist_ok=True) + os.makedirs(version_level_path, exist_ok=True) # Save metadata.json - metadata_path = os.path.join(base_path, 'metadata.json') + metadata_path = os.path.join(version_level_path, 'metadata.json') metadata = { 'name': data.get('name'), 'publisher': offer_content.get('offerPublisher'), @@ -106,12 +132,17 @@ def get_offer(cmd, # Download icons if icon_uris: for size, uri in icon_uris.items(): + file_extension = 'png' + file_path = os.path.join(icon_path, f"{size}.{file_extension}") + + # Skip if icon already exists + if os.path.exists(file_path): + logger.info(f"Icon {size} already exists at {file_path}, skipping download") + continue + try: logo_response = requests.get(uri) if logo_response.status_code == 200: - file_extension = 'png' - file_path = os.path.join(icon_path, f"{size}.{file_extension}") - with open(file_path, 'wb') as f: f.write(logo_response.content) logger.info(f"Downloaded {size} logo to {file_path}") @@ -120,34 +151,7 @@ def get_offer(cmd, except Exception as e: logger.error(f"Error downloading {size} logo: {str(e)}") - - # Format offer details - result = { - 'name': data.get('name'), - 'publisher': offer_content.get('offerPublisher', {}).get('publisherDisplayName'), - 'offer_id': offer_content.get('offerId'), - 'summary': offer_content.get('summary'), - 'description': offer_content.get('description'), - 'skus': [] - } - - # Add SKU information - skus = data.get('properties', {}).get('marketplaceSkus', []) - for sku in skus: - sku_info = { - 'name': sku.get('displayName'), - 'id': sku.get('marketplaceSkuId'), - 'os_type': sku.get('operatingSystem', {}).get('type'), - 'versions': [ - { - 'version': v.get('name'), - 'size_mb': v.get('minimumDownloadSizeInMb') - } for v in sku.get('marketplaceSkuVersions', [])[:3] # Latest 3 versions - ] - } - result['skus'].append(sku_info) - - return result + print ("Metadata and icons downloaded successfully") else: error_message = f"Request failed with status code: {response.status_code}" @@ -166,91 +170,23 @@ def get_offer(cmd, 'status': 'failed', 'resource_group_name': resource_group_name } - -def get_image_download_url(cmd, - resource_group_name, - publisher, - offer, - sku, - version, - management_endpoint=None, - provider_namespace="Microsoft.Edge", - api_version="2024-11-01-preview"): - """Get download URL for a specific marketplace image version.""" - - from azure.cli.core.commands.client_factory import get_subscription_id - from azure.cli.core.util import send_raw_request - from knack.log import get_logger - - if management_endpoint is None: - management_endpoint = _get_management_endpoint(provider_namespace) - - logger = get_logger(__name__) - - # Get subscription ID - subscription_id = get_subscription_id(cmd.cli_ctx) - - # Construct URL for the listDownloadUri API - url = ( - f"https://{management_endpoint}" - f"/subscriptions/{subscription_id}" - f"/resourceGroups/{resource_group_name}" - f"/providers/{provider_namespace}/disconnectedOperations/demo-winfield1" - f"/images/{publisher}.{offer}.{sku}.{version}/listDownloadUri" - f"?api-version={api_version}" - ) - - try: - # Make POST request to get download URL - response = send_raw_request(cmd.cli_ctx, 'post', url, - resource="https://management.azure.com") - - if response.status_code == 200: - download_info = response.json() - return { - 'download_url': download_info.get('downloadUri'), - 'expiry': download_info.get('expiryTime'), - 'publisher': publisher, - 'offer': offer, - 'sku': sku, - 'version': version - } - else: - error_message = f"Failed to get download URL. Status code: {response.status_code}" - logger.error(error_message) - return { - 'error': error_message, - 'status': 'failed', - 'response': response.text - } - - except Exception as e: - logger.error(f"Error getting download URL: {str(e)}") - return { - 'error': str(e), - 'status': 'failed' - } -def list_offers(cmd, - resource_group_name, - management_endpoint=None, - provider_namespace="Microsoft.Edge", - sub_provider="Microsoft.EdgeMarketPlace", - api_version="2023-08-01-preview"): +def list_offers(cmd, resource_group_name): """List all offers for disconnected operations.""" - from azure.cli.core.profiles import ResourceType from azure.cli.core.commands.client_factory import get_subscription_id from azure.cli.core.util import send_raw_request from knack.log import get_logger logger = get_logger(__name__) - if management_endpoint is None: - management_endpoint = _get_management_endpoint(provider_namespace) + management_endpoint = _get_management_endpoint() # Get subscription ID from current context subscription_id = get_subscription_id(cmd.cli_ctx) + provider_namespace="Private.EdgeInternal" + sub_provider="Microsoft.EdgeMarketPlace" + api_version="2023-08-01-preview" # Construct URL with parameters url = ( @@ -275,27 +211,30 @@ def list_offers(cmd, if response.status_code == 200: data = response.json() - - # Format data for output result = [] + for offer in data.get('value', []): offer_content = offer.get('properties', {}).get('offerContent', {}) skus = offer.get('properties', {}).get('marketplaceSkus', []) for sku in skus: - versions = sku.get('marketplaceSkuVersions', []) - for version in versions[:3]: # Show only latest 3 versions - row = { - 'Publisher': offer_content.get('offerPublisher', {}).get('publisherDisplayName'), - 'Offer': offer_content.get('offerId'), - 'SKU': sku.get('marketplaceSkuId'), - 'Version': version.get('name'), - 'OS_Type': sku.get('operatingSystem', {}).get('type'), - 'Size_MB': version.get('minimumDownloadSizeInMb') - } - result.append(row) + versions = sku.get('marketplaceSkuVersions', [])[:] + # Format versions as comma-separated string with size + version_str = ', '.join([f"{v.get('name')}({v.get('minimumDownloadSizeInMb')}MB)" + for v in versions]) + + # Create a single row with flattened version info + row = { + 'Publisher': offer_content.get('offerPublisher', {}).get('publisherId'), + 'Offer': offer_content.get('offerId'), + 'SKU': sku.get('marketplaceSkuId'), + 'Versions': version_str, + 'OS_Type': sku.get('operatingSystem', {}).get('type') + } + result.append(row) return result + else: error_message = f"Request failed with status code: {response.status_code}" logger.error(error_message) From bdd0065bb7c3b1907b3089ddce0fa5d726009854 Mon Sep 17 00:00:00 2001 From: Sankalp Kotewar Date: Tue, 18 Feb 2025 15:49:28 +0530 Subject: [PATCH 04/24] fixed linter comments --- .../disconnectedoperations/_help.py | 128 ++++-------------- .../disconnectedoperations/commands.py | 1 - 2 files changed, 24 insertions(+), 105 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py index 3692f1572b1..18a3361d09e 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py @@ -1,136 +1,56 @@ -from knack.help_files import helps # pylint: disable=unused-import +from knack.help_files import helps helps['disconnectedoperations'] = """ type: group - short-summary: Commands to manage disconnected operations. - long-summary: Manage Azure Edge marketplace operations in disconnected environments. + short-summary: Commands to manage Azure Disconnected Operations. + long-summary: Manage Azure Disconnected Operations for Edge marketplace offers. """ helps['disconnectedoperations edgemarketplace'] = """ type: group - short-summary: Manage Edge Marketplace operations. - long-summary: Commands to manage Edge Marketplace images and offers. + short-summary: Manage Edge marketplace offers for disconnected operations. + long-summary: Commands to list, get details, and package Edge marketplace offers for disconnected operations. """ helps['disconnectedoperations edgemarketplace listoffers'] = """ type: command - short-summary: List available marketplace offers. - long-summary: List all available marketplace offers with their SKUs and versions. - parameters: - - name: --resource-group -g - type: string - required: true - short-summary: Name of resource group. - - name: --management-endpoint - type: string - short-summary: Management endpoint URL. - long-summary: Uses brazilus.management.azure.com for test environment, management.azure.com for production. - default: management.azure.com - - name: --provider-namespace - type: string - short-summary: Provider namespace. - long-summary: Use "Private.EdgeInternal" for test environment or "Microsoft.Edge" for production. - default: Microsoft.Edge - - name: --sub-provider - type: string - short-summary: Sub-provider namespace. - default: Microsoft.EdgeMarketPlace - - name: --api-version - type: string - short-summary: API version to use. - default: 2023-08-01-preview + short-summary: List all available Edge marketplace offers. + long-summary: List all available Edge marketplace offers with their publishers, SKUs, and versions. examples: - - name: List offers using production environment + - name: List all offers in a resource group text: az disconnectedoperations edgemarketplace listoffers -g myResourceGroup - - name: List offers using test environment - text: az disconnectedoperations edgemarketplace listoffers -g myResourceGroup --provider-namespace Private.EdgeInternal - - name: List offers in table format - text: az disconnectedoperations edgemarketplace listoffers -g myResourceGroup --output table """ -helps['disconnectedoperations edgemarketplace get-offer'] = """ +helps['disconnectedoperations edgemarketplace packageoffer'] = """ type: command - short-summary: Get details of a specific marketplace offer. - long-summary: Retrieve detailed information about a marketplace offer and optionally download its logos. + short-summary: Package an Edge marketplace offer for disconnected operations. + long-summary: Download and package an Edge marketplace offer including its metadata, logos, and other artifacts. parameters: - name: --resource-group -g type: string - required: true short-summary: Name of resource group. - - name: --offer-name - type: string - required: true - short-summary: Name of the offer to retrieve. - - name: --output-folder - type: string - short-summary: Local folder path to save logos and metadata. - - name: --management-endpoint - type: string - short-summary: Management endpoint URL. - long-summary: Uses brazilus.management.azure.com for test environment, management.azure.com for production. - default: management.azure.com - - name: --provider-namespace - type: string - short-summary: Provider namespace. - long-summary: Use "Private.EdgeInternal" for test environment or "Microsoft.Edge" for production. - default: Microsoft.Edge - - name: --sub-provider - type: string - short-summary: Sub-provider namespace. - default: Microsoft.EdgeMarketPlace - - name: --api-version - type: string - short-summary: API version to use. - default: 2023-08-01-preview - examples: - - name: Get offer details using production environment - text: az disconnectedoperations edgemarketplace get-offer -g myResourceGroup --offer-name myOffer - - name: Get offer details and save logos using test environment - text: az disconnectedoperations edgemarketplace get-offer -g myResourceGroup --offer-name myOffer --output-folder ./artifacts --provider-namespace Private.EdgeInternal -""" - -helps['disconnectedoperations edgemarketplace get-image-download-url'] = """ - type: command - short-summary: Get download URL for a marketplace image. - long-summary: Get the download URL for a specific marketplace image version. - parameters: - - name: --resource-group -g - type: string required: true - short-summary: Name of resource group. - - name: --publisher + - name: --publisher-name type: string + short-summary: Name of the publisher. required: true - short-summary: Publisher of the marketplace image. - - name: --offer + - name: --offer-name type: string + short-summary: Name of the offer. required: true - short-summary: Offer name of the marketplace image. - name: --sku type: string - required: true - short-summary: SKU identifier. + short-summary: SKU of the offer. - name: --version type: string - required: true - short-summary: Version of the marketplace image. - - name: --management-endpoint - type: string - short-summary: Management endpoint URL. - long-summary: Uses brazilus.management.azure.com for test environment, management.azure.com for production. - default: management.azure.com - - name: --provider-namespace - type: string - short-summary: Provider namespace. - long-summary: Use "Private.EdgeInternal" for test environment or "Microsoft.Edge" for production. - default: Microsoft.Edge - - name: --api-version + short-summary: Version of the offer. If not specified, latest version will be used. + - name: --output-folder type: string - short-summary: API version to use. - default: 2024-11-01-preview + short-summary: Output folder path for downloaded artifacts. + required: true examples: - - name: Get image download URL using production environment - text: az disconnectedoperations edgemarketplace get-image-download-url -g myResourceGroup --publisher MicrosoftWindowsServer --offer WindowsServer --sku 2019-Datacenter --version latest - - name: Get image download URL using test environment - text: az disconnectedoperations edgemarketplace get-image-download-url -g myResourceGroup --publisher MicrosoftWindowsServer --offer WindowsServer --sku 2019-Datacenter --version latest --provider-namespace Private.EdgeInternal + - name: Package latest version of an offer + text: az disconnectedoperations edgemarketplace packageoffer -g myResourceGroup --publisher-name publisherName --offer-name offerName --output-folder ./output + - name: Package specific version of an offer + text: az disconnectedoperations edgemarketplace packageoffer -g myResourceGroup --publisher-name publisherName --offer-name offerName --version 1.0.0 --output-folder ./output """ \ No newline at end of file diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py index 57bd67d294e..609de09ab4e 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py @@ -46,7 +46,6 @@ def load_command_table(self, _): with self.command_group('disconnectedoperations edgemarketplace', custom_command_type=custom_command_type) as g: g.custom_command('listoffers', 'list_offers', table_transformer=transform_offers_table) - g.custom_command('getoffer', 'get_offer') g.custom_command('packageoffer', 'package_offer') return self.command_table \ No newline at end of file From 073d0d375d5e27afa980c9b4e79ba38aafc0cfcb Mon Sep 17 00:00:00 2001 From: Sankalp Kotewar Date: Wed, 19 Feb 2025 16:49:38 +0530 Subject: [PATCH 05/24] added get-offer --- .../disconnectedoperations/_help.py | 111 +++++++++++++----- .../disconnectedoperations/commands.py | 20 +++- .../disconnectedoperations/custom.py | 110 ++++++++++++++--- 3 files changed, 196 insertions(+), 45 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py index 18a3361d09e..102459b07e4 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py @@ -1,56 +1,109 @@ -from knack.help_files import helps +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- -helps['disconnectedoperations'] = """ - type: group - short-summary: Commands to manage Azure Disconnected Operations. - long-summary: Manage Azure Disconnected Operations for Edge marketplace offers. -""" +from knack.help_files import helps helps['disconnectedoperations edgemarketplace'] = """ type: group - short-summary: Manage Edge marketplace offers for disconnected operations. - long-summary: Commands to list, get details, and package Edge marketplace offers for disconnected operations. + short-summary: Manage Edge Marketplace offers for disconnected operations. + long-summary: Commands to list, get details, and package marketplace offers for disconnected operations. """ helps['disconnectedoperations edgemarketplace listoffers'] = """ type: command - short-summary: List all available Edge marketplace offers. - long-summary: List all available Edge marketplace offers with their publishers, SKUs, and versions. + short-summary: List all available marketplace offers. examples: - - name: List all offers in a resource group - text: az disconnectedoperations edgemarketplace listoffers -g myResourceGroup + - name: List all marketplace offers for a specific resource + text: > + az disconnectedoperations edgemarketplace listoffers --resource-group myResourceGroup --resource-name myResource + - name: List offers and format output as table + text: > + az disconnectedoperations edgemarketplace listoffers -g myResourceGroup -n myResource --output table + - name: List offers and filter output using JMESPath query + text: > + az disconnectedoperations edgemarketplace listoffers -g myResourceGroup -n myResource --query "[?OS_Type=='Linux']" + parameters: + - name: --resource-group -g + type: string + short-summary: Name of resource group + - name: --resource-name -n + type: string + short-summary: The resource name +""" + +helps['disconnectedoperations edgemarketplace getoffer'] = """ + type: command + short-summary: Get details of a specific marketplace offer. + examples: + - name: Get details of a specific marketplace offer + text: > + az disconnectedoperations edgemarketplace getoffer --resource-group myResourceGroup --resource-name myResource + --publisher-name publisherName --offer-name offerName + - name: Get offer details and output as JSON + text: > + az disconnectedoperations edgemarketplace getoffer -g myResourceGroup -n myResource + --publisher-name publisherName --offer-name offerName --output json + - name: Get offer details with custom query + text: > + az disconnectedoperations edgemarketplace getoffer -g myResourceGroup -n myResource + --publisher-name publisherName --offer-name offerName --query "[].{SKU:SKU,Version:Versions}" + parameters: + - name: --resource-group -g + type: string + short-summary: Name of resource group + - name: --resource-name -n + type: string + short-summary: The resource name + - name: --publisher-name + type: string + short-summary: The publisher name of the offer + - name: --offer-name + type: string + short-summary: The name of the offer """ helps['disconnectedoperations edgemarketplace packageoffer'] = """ type: command - short-summary: Package an Edge marketplace offer for disconnected operations. - long-summary: Download and package an Edge marketplace offer including its metadata, logos, and other artifacts. + short-summary: Download and package a marketplace offer with its metadata and icons. + long-summary: Downloads the marketplace offer metadata, icons, and creates a package in the specified output folder. + examples: + - name: Package a marketplace offer with specific version + text: > + az disconnectedoperations edgemarketplace packageoffer --resource-group myResourceGroup --resource-name myResource + --publisher-name publisherName --offer-name offerName --sku skuName --version versionNumber + --output-folder ./output + - name: Package latest version of an offer + text: > + az disconnectedoperations edgemarketplace packageoffer -g myResourceGroup -n myResource + --publisher-name publisherName --offer-name offerName --sku skuName + --output-folder ./latest-package + - name: Package an offer and save to a specific directory + text: > + az disconnectedoperations edgemarketplace packageoffer -g myResourceGroup -n myResource + --publisher-name publisherName --offer-name offerName --sku skuName + --output-folder "D:\\MarketplacePackages" parameters: - name: --resource-group -g type: string - short-summary: Name of resource group. - required: true + short-summary: Name of resource group + - name: --resource-name -n + type: string + short-summary: The resource name - name: --publisher-name type: string - short-summary: Name of the publisher. - required: true + short-summary: The publisher name of the offer - name: --offer-name type: string - short-summary: Name of the offer. - required: true + short-summary: The name of the offer - name: --sku type: string - short-summary: SKU of the offer. + short-summary: The SKU of the offer - name: --version type: string - short-summary: Version of the offer. If not specified, latest version will be used. + short-summary: The version of the offer (optional, latest version will be used if not specified) - name: --output-folder type: string - short-summary: Output folder path for downloaded artifacts. - required: true - examples: - - name: Package latest version of an offer - text: az disconnectedoperations edgemarketplace packageoffer -g myResourceGroup --publisher-name publisherName --offer-name offerName --output-folder ./output - - name: Package specific version of an offer - text: az disconnectedoperations edgemarketplace packageoffer -g myResourceGroup --publisher-name publisherName --offer-name offerName --version 1.0.0 --output-folder ./output + short-summary: The folder path where the package will be downloaded """ \ No newline at end of file diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py index 609de09ab4e..2bb27f47f1c 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py @@ -13,6 +13,24 @@ def transform_offers_table(result): if not result: return result + # Transform each row while preserving order + transformed = [] + for item in result: + row = OrderedDict([ + ('Publisher', item['Publisher']), + ('Offer', item['Offer']), + ('SKU', item['SKU']), + ('Version', item['Versions']), + ('OS_Type', item['OS_Type']) + ]) + transformed.append(row) + + return transformed + +def transform_offer_table(result): + if not result: + return result + # Transform each row while preserving order transformed = [] for item in result: @@ -27,7 +45,6 @@ def transform_offers_table(result): formatted_versions = '\n'.join(str(v).strip() for v in versions) else: formatted_versions = str(versions) - row = OrderedDict([ ('Publisher', item['Publisher']), ('Offer', item['Offer']), @@ -46,6 +63,7 @@ def load_command_table(self, _): with self.command_group('disconnectedoperations edgemarketplace', custom_command_type=custom_command_type) as g: g.custom_command('listoffers', 'list_offers', table_transformer=transform_offers_table) + g.custom_command('getoffer', 'get_offer', table_transformer=transform_offer_table) g.custom_command('packageoffer', 'package_offer') return self.command_table \ No newline at end of file diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py index 408fe8b0978..1f855182ef5 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py @@ -12,13 +12,15 @@ logger = get_logger(__name__) -def _get_management_endpoint(): - """Helper function to determine management endpoint based on provider namespace.""" - return "brazilus.management.azure.com" # if provider_namespace == "Private.EdgeInternal" else "management.azure.com" - +def _get_management_endpoint(cli_ctx): + """Helper function to determine management endpoint based on cloud configuration.""" + # cloud = cli_ctx.cloud + # return cloud.endpoints.resource_manager + return "brazilus.management.azure.com" # For testing purposes def package_offer(cmd, resource_group_name, + resource_name, publisher_name, offer_name, sku, @@ -34,7 +36,7 @@ def package_offer(cmd, from knack.log import get_logger # Use helper function if management_endpoint not explicitly provided - management_endpoint = _get_management_endpoint() + management_endpoint = _get_management_endpoint(cmd.cli_ctx) logger = get_logger(__name__) # Get subscription ID from current context @@ -49,7 +51,7 @@ def package_offer(cmd, f"https://{management_endpoint}" f"/subscriptions/{subscription_id}" f"/resourceGroups/{resource_group_name}" - f"/providers/{provider_namespace}/disconnectedOperations/demo-winfield1" + f"/providers/Microsoft.DataBoxEdge/dataBoxEdgeDevices/{resource_name}" f"/providers/{sub_provider}/offers/{publisher_name}:{offer_name}" f"?api-version={api_version}" ) @@ -171,7 +173,7 @@ def package_offer(cmd, 'resource_group_name': resource_group_name } -def list_offers(cmd, resource_group_name): +def list_offers(cmd, resource_group_name, resource_name): """List all offers for disconnected operations.""" from azure.cli.core.commands.client_factory import get_subscription_id @@ -180,7 +182,7 @@ def list_offers(cmd, resource_group_name): logger = get_logger(__name__) - management_endpoint = _get_management_endpoint() + management_endpoint = _get_management_endpoint(cmd.cli_ctx) # Get subscription ID from current context subscription_id = get_subscription_id(cmd.cli_ctx) @@ -193,7 +195,7 @@ def list_offers(cmd, resource_group_name): f"https://{management_endpoint}" f"/subscriptions/{subscription_id}" f"/resourceGroups/{resource_group_name}" - f"/providers/{provider_namespace}/disconnectedOperations/demo-winfield1" + f"/providers/Microsoft.DataBoxEdge/dataBoxEdgeDevices/{resource_name}" f"/providers/{sub_provider}/offers" f"?api-version={api_version}" ) @@ -219,16 +221,11 @@ def list_offers(cmd, resource_group_name): for sku in skus: versions = sku.get('marketplaceSkuVersions', [])[:] - # Format versions as comma-separated string with size - version_str = ', '.join([f"{v.get('name')}({v.get('minimumDownloadSizeInMb')}MB)" - for v in versions]) - - # Create a single row with flattened version info row = { 'Publisher': offer_content.get('offerPublisher', {}).get('publisherId'), 'Offer': offer_content.get('offerId'), 'SKU': sku.get('marketplaceSkuId'), - 'Versions': version_str, + 'Versions': f"{len(versions)} {'version' if len(versions) == 1 else 'versions'} available", 'OS_Type': sku.get('operatingSystem', {}).get('type') } result.append(row) @@ -245,6 +242,89 @@ def list_offers(cmd, resource_group_name): 'response': response.text } + except Exception as e: + logger.error(f"Failed to retrieve offers: {str(e)}") + return { + 'error': str(e), + 'status': 'failed', + 'resource_group_name': resource_group_name + } + +def get_offer(cmd, resource_group_name, resource_name, publisher_name, offer_name): + """List all offers for disconnected operations.""" + + from azure.cli.core.commands.client_factory import get_subscription_id + from azure.cli.core.util import send_raw_request + from knack.log import get_logger + + logger = get_logger(__name__) + + management_endpoint = _get_management_endpoint(cmd.cli_ctx) + + # Get subscription ID from current context + subscription_id = get_subscription_id(cmd.cli_ctx) + provider_namespace="Private.EdgeInternal" + sub_provider="Microsoft.EdgeMarketPlace" + api_version="2023-08-01-preview" + + # Construct URL with parameters + url = ( + f"https://{management_endpoint}" + f"/subscriptions/{subscription_id}" + f"/resourceGroups/{resource_group_name}" + f"/providers/Microsoft.DataBoxEdge/dataBoxEdgeDevices/{resource_name}" + f"/providers/{sub_provider}/offers/{publisher_name}:{offer_name}" + f"?api-version={api_version}" + ) + + # Define headers with resource for authentication + headers = { + 'Content-Type': 'application/json', + } + + # Define the resource for authentication + resource = "https://management.azure.com" # Using standard Azure management endpoint + + try: + response = send_raw_request(cmd.cli_ctx, 'get', url, resource=resource) + + if response.status_code == 200: + data = response.json() + result = [] + + + offer_content = data.get('properties', {}).get('offerContent', {}) + skus = data.get('properties', {}).get('marketplaceSkus', []) + + for sku in skus: + # Get all versions for this SKU + versions = sku.get('marketplaceSkuVersions', [])[:] + + # transform versions and size array into a multi-line string + version_str = ', '.join([f"{v.get('name')}({v.get('minimumDownloadSizeInMb')}MB)" + for v in versions]) + + # Create a single row with flattened version info + row = { + 'Publisher': offer_content.get('offerPublisher', {}).get('publisherId'), + 'Offer': offer_content.get('offerId'), + 'SKU': sku.get('marketplaceSkuId'), + 'Versions': version_str, + 'OS_Type': sku.get('operatingSystem', {}).get('type') + } + result.append(row) + return result + + else: + error_message = f"Request failed with status code: {response.status_code}" + logger.error(error_message) + return { + 'error': error_message, + 'status': 'failed', + 'resource_group_name': resource_group_name, + 'response': response.text + } + except Exception as e: logger.error(f"Failed to retrieve offers: {str(e)}") return { From ded88198658f9be481aee7e69a7116b831d4a074 Mon Sep 17 00:00:00 2001 From: Sankalp Kotewar Date: Wed, 19 Feb 2025 17:17:55 +0530 Subject: [PATCH 06/24] updated help file --- .../disconnectedoperations/_help.py | 29 ++++++++----------- .../disconnectedoperations/_params.py | 17 +++-------- 2 files changed, 16 insertions(+), 30 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py index 102459b07e4..910fa2075e3 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py @@ -5,6 +5,11 @@ from knack.help_files import helps +helps['disconnectedoperations'] = """ + type: group + short-summary: Manage disconnected operations. + long-summary: Commands to list, get details, and package marketplace offers for disconnected operations. +""" helps['disconnectedoperations edgemarketplace'] = """ type: group short-summary: Manage Edge Marketplace offers for disconnected operations. @@ -20,15 +25,15 @@ az disconnectedoperations edgemarketplace listoffers --resource-group myResourceGroup --resource-name myResource - name: List offers and format output as table text: > - az disconnectedoperations edgemarketplace listoffers -g myResourceGroup -n myResource --output table + az disconnectedoperations edgemarketplace listoffers -g myResourceGroup --resource-name myResource --output table - name: List offers and filter output using JMESPath query text: > - az disconnectedoperations edgemarketplace listoffers -g myResourceGroup -n myResource --query "[?OS_Type=='Linux']" + az disconnectedoperations edgemarketplace listoffers -g myResourceGroup --resource-name myResource --query "[?OS_Type=='Linux']" parameters: - name: --resource-group -g type: string short-summary: Name of resource group - - name: --resource-name -n + - name: --resource-name type: string short-summary: The resource name """ @@ -43,17 +48,17 @@ --publisher-name publisherName --offer-name offerName - name: Get offer details and output as JSON text: > - az disconnectedoperations edgemarketplace getoffer -g myResourceGroup -n myResource + az disconnectedoperations edgemarketplace getoffer -g myResourceGroup --resource-name myResource --publisher-name publisherName --offer-name offerName --output json - name: Get offer details with custom query text: > - az disconnectedoperations edgemarketplace getoffer -g myResourceGroup -n myResource + az disconnectedoperations edgemarketplace getoffer -g myResourceGroup --resource-name myResource --publisher-name publisherName --offer-name offerName --query "[].{SKU:SKU,Version:Versions}" parameters: - name: --resource-group -g type: string short-summary: Name of resource group - - name: --resource-name -n + - name: --resource-name type: string short-summary: The resource name - name: --publisher-name @@ -73,22 +78,12 @@ text: > az disconnectedoperations edgemarketplace packageoffer --resource-group myResourceGroup --resource-name myResource --publisher-name publisherName --offer-name offerName --sku skuName --version versionNumber - --output-folder ./output - - name: Package latest version of an offer - text: > - az disconnectedoperations edgemarketplace packageoffer -g myResourceGroup -n myResource - --publisher-name publisherName --offer-name offerName --sku skuName - --output-folder ./latest-package - - name: Package an offer and save to a specific directory - text: > - az disconnectedoperations edgemarketplace packageoffer -g myResourceGroup -n myResource - --publisher-name publisherName --offer-name offerName --sku skuName --output-folder "D:\\MarketplacePackages" parameters: - name: --resource-group -g type: string short-summary: Name of resource group - - name: --resource-name -n + - name: --resource-name type: string short-summary: The resource name - name: --publisher-name diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py index 166d6ed135d..b259919fe82 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py @@ -6,31 +6,22 @@ # -------------------------------------------------------------------------------------------- from azure.cli.core.commands.parameters import resource_group_name_type -from knack.arguments import CLIArgumentType -def load_arguments(self, _): # pylint: disable=unused-argument - provider_namespace_type = CLIArgumentType( - type=str, - help='Provider namespace. Use "Private.EdgeInternal" for test environment or "Microsoft.EdgeMarketplace" for production', - default="Private.EdgeInternal" - ) - - management_endpoint_type = CLIArgumentType( - type=str, - help='Management endpoint URL. Use brazilus.management.azure.com for test environment, management.azure.com for production', - default="brazilus.management.azure.com" - ) +def load_arguments(self, _): with self.argument_context('disconnectedoperations edgemarketplace listoffers') as c: c.argument('resource_group_name', arg_type=resource_group_name_type) + c.argument('resource_name', type=str, help='Name of the resource to list offers for') with self.argument_context('disconnectedoperations edgemarketplace getoffer') as c: c.argument('resource_group_name', arg_type=resource_group_name_type) + c.argument('resource_name', type=str, help='Name of the resource to list offers for') c.argument('offer_name', type=str, help='Name of the offer to retrieve') c.argument('product_name', type=str, help='Name of the product to retrieve') with self.argument_context('disconnectedoperations edgemarketplace packageoffer') as c: c.argument('resource_group_name', arg_type=resource_group_name_type) + c.argument('resource_name', type=str, help='Name of the resource to list offers for') c.argument('publisher_name', type=str, help='Name of the publisher') c.argument('offer_name', type=str, help='Name of the offer to package') c.argument('sku', type=str, help='SKU of the product to retrieve') From f084e8ca759d9dbc7de725ac716c718e4b9bbf25 Mon Sep 17 00:00:00 2001 From: Sankalp Kotewar Date: Tue, 25 Feb 2025 18:12:28 +0530 Subject: [PATCH 07/24] Added image download logic --- .../disconnectedoperations/__init__.py | 32 +- .../disconnectedoperations/_client_factory.py | 8 +- .../disconnectedoperations/_help.py | 168 +++-- .../disconnectedoperations/_params.py | 48 +- .../disconnectedoperations/commands.py | 76 ++- .../disconnectedoperations/custom.py | 633 +++++++++++++----- 6 files changed, 631 insertions(+), 334 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/__init__.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/__init__.py index 1aaa566929a..07ea9258bcb 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/__init__.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/__init__.py @@ -5,30 +5,40 @@ # Code generated by aaz-dev-tools # -------------------------------------------------------------------------------------------- -from azure.cli.core import AzCommandsLoader -from azure.cli.command_modules.disconnectedoperations._help import helps # pylint: disable=unused-import from azure.cli.command_modules.disconnectedoperations._client_factory import cf_image +from azure.cli.core import AzCommandsLoader class DisconnectedoperationsCommandsLoader(AzCommandsLoader): - def __init__(self, cli_ctx=None): from azure.cli.core.commands import CliCommandType - from azure.cli.core.profiles import ResourceType # required when using python sdk + from azure.cli.core.profiles import ( + ResourceType, # required when using python sdk + ) + disconnectedoperations_custom = CliCommandType( - operations_tmpl='azure.cli.command_modules.disconnectedoperations.custom#{}', - client_factory=cf_image) - super(DisconnectedoperationsCommandsLoader, self).__init__(cli_ctx=cli_ctx, - resource_type=ResourceType.MGMT_DISCONNECTEDOPERATIONS, # required when using python sdk - custom_command_type=disconnectedoperations_custom) + operations_tmpl="azure.cli.command_modules.disconnectedoperations.custom#{}", + client_factory=cf_image, + ) + super().__init__( + cli_ctx=cli_ctx, + resource_type=ResourceType.MGMT_DISCONNECTEDOPERATIONS, + custom_command_type=disconnectedoperations_custom, + ) def load_command_table(self, args): - from azure.cli.command_modules.disconnectedoperations.commands import load_command_table + from azure.cli.command_modules.disconnectedoperations.commands import ( + load_command_table, + ) + load_command_table(self, args) return self.command_table def load_arguments(self, command): - from azure.cli.command_modules.disconnectedoperations._params import load_arguments + from azure.cli.command_modules.disconnectedoperations._params import ( + load_arguments, + ) + load_arguments(self, command) diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_client_factory.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_client_factory.py index 20df0404d3b..ab3fbf60d18 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_client_factory.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_client_factory.py @@ -7,15 +7,9 @@ def get_disconnectedoperations_management_client(cli_ctx, *_): from azure.cli.core.commands.client_factory import get_mgmt_service_client from azure.mgmt.disconnectedoperations import DisconnectedOperationsClient + return get_mgmt_service_client(cli_ctx, DisconnectedOperationsClient) def cf_image(cli_ctx, *_): return get_disconnectedoperations_management_client(cli_ctx).image - -def cf_logos(cli_ctx, *_): - return get_disconnectedoperations_management_client(cli_ctx).logos - -def cf_metadata(cli_ctx, *_): - return get_disconnectedoperations_management_client(cli_ctx).metadata - diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py index 910fa2075e3..644668227e5 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py @@ -6,99 +6,97 @@ from knack.help_files import helps helps['disconnectedoperations'] = """ - type: group - short-summary: Manage disconnected operations. - long-summary: Commands to list, get details, and package marketplace offers for disconnected operations. +type: group +short-summary: Manage disconnected operations. """ helps['disconnectedoperations edgemarketplace'] = """ - type: group - short-summary: Manage Edge Marketplace offers for disconnected operations. - long-summary: Commands to list, get details, and package marketplace offers for disconnected operations. +type: group +short-summary: Manage Edge Marketplace offers for disconnected operations. """ helps['disconnectedoperations edgemarketplace listoffers'] = """ - type: command - short-summary: List all available marketplace offers. - examples: - - name: List all marketplace offers for a specific resource - text: > - az disconnectedoperations edgemarketplace listoffers --resource-group myResourceGroup --resource-name myResource - - name: List offers and format output as table - text: > - az disconnectedoperations edgemarketplace listoffers -g myResourceGroup --resource-name myResource --output table - - name: List offers and filter output using JMESPath query - text: > - az disconnectedoperations edgemarketplace listoffers -g myResourceGroup --resource-name myResource --query "[?OS_Type=='Linux']" - parameters: - - name: --resource-group -g - type: string - short-summary: Name of resource group - - name: --resource-name - type: string - short-summary: The resource name +type: command +short-summary: List all available marketplace offers. +examples: +- name: List all marketplace offers for a specific resource + text: > +az disconnectedoperations edgemarketplace listoffers --resource-group myResourceGroup --resource-name myResource +- name: List offers and format output as table + text: > +az disconnectedoperations edgemarketplace listoffers -g myResourceGroup --resource-name myResource --output table +- name: List offers and filter output using JMESPath query + text: > +az disconnectedoperations edgemarketplace listoffers -g myResourceGroup --resource-name myResource --query "[?OS_Type=='Linux']" +parameters: +- name: --resource-group -g + type: string + short-summary: Name of resource group +- name: --resource-name + type: string + short-summary: The resource name """ helps['disconnectedoperations edgemarketplace getoffer'] = """ - type: command - short-summary: Get details of a specific marketplace offer. - examples: - - name: Get details of a specific marketplace offer - text: > - az disconnectedoperations edgemarketplace getoffer --resource-group myResourceGroup --resource-name myResource - --publisher-name publisherName --offer-name offerName - - name: Get offer details and output as JSON - text: > - az disconnectedoperations edgemarketplace getoffer -g myResourceGroup --resource-name myResource - --publisher-name publisherName --offer-name offerName --output json - - name: Get offer details with custom query - text: > - az disconnectedoperations edgemarketplace getoffer -g myResourceGroup --resource-name myResource - --publisher-name publisherName --offer-name offerName --query "[].{SKU:SKU,Version:Versions}" - parameters: - - name: --resource-group -g - type: string - short-summary: Name of resource group - - name: --resource-name - type: string - short-summary: The resource name - - name: --publisher-name - type: string - short-summary: The publisher name of the offer - - name: --offer-name - type: string - short-summary: The name of the offer +type: command +short-summary: Get details of a specific marketplace offer. +examples: +- name: Get details of a specific marketplace offer + text: > +az disconnectedoperations edgemarketplace getoffer --resource-group myResourceGroup --resource-name myResource +--publisher-name publisherName --offer-name offerName +- name: Get offer details and output as JSON + text: > +az disconnectedoperations edgemarketplace getoffer -g myResourceGroup --resource-name myResource +--publisher-name publisherName --offer-name offerName --output json +- name: Get offer details with custom query + text: > +az disconnectedoperations edgemarketplace getoffer -g myResourceGroup --resource-name myResource +--publisher-name publisherName --offer-name offerName --query "[].{SKU:SKU,Version:Versions}" +parameters: +- name: --resource-group -g + type: string + short-summary: Name of resource group +- name: --resource-name + type: string + short-summary: The resource name +- name: --publisher-name + type: string + short-summary: The publisher name of the offer +- name: --offer-name + type: string + short-summary: The name of the offer """ helps['disconnectedoperations edgemarketplace packageoffer'] = """ - type: command - short-summary: Download and package a marketplace offer with its metadata and icons. - long-summary: Downloads the marketplace offer metadata, icons, and creates a package in the specified output folder. - examples: - - name: Package a marketplace offer with specific version - text: > - az disconnectedoperations edgemarketplace packageoffer --resource-group myResourceGroup --resource-name myResource - --publisher-name publisherName --offer-name offerName --sku skuName --version versionNumber - --output-folder "D:\\MarketplacePackages" - parameters: - - name: --resource-group -g - type: string - short-summary: Name of resource group - - name: --resource-name - type: string - short-summary: The resource name - - name: --publisher-name - type: string - short-summary: The publisher name of the offer - - name: --offer-name - type: string - short-summary: The name of the offer - - name: --sku - type: string - short-summary: The SKU of the offer - - name: --version - type: string - short-summary: The version of the offer (optional, latest version will be used if not specified) - - name: --output-folder - type: string - short-summary: The folder path where the package will be downloaded -""" \ No newline at end of file +type: command +short-summary: Download and package a marketplace offer with its metadata and icons. +long-summary: Downloads the marketplace offer metadata, icons, and creates a package in the specified output folder. +examples: +- name: Package a marketplace offer with specific version + text: > +az disconnectedoperations edgemarketplace packageoffer --resource-group myResourceGroup --resource-name myResource +--publisher-name publisherName --offer-name offerName --sku skuName --version versionNumber +--output-folder "D:\\MarketplacePackages" +parameters: +- name: --resource-group -g + type: string + short-summary: Name of resource group +- name: --resource-name + type: string + short-summary: The resource name +- name: --publisher-name + type: string + short-summary: The publisher name of the offer +- name: --offer-name + type: string + short-summary: The name of the offer +- name: --sku + type: string + short-summary: The SKU of the offer +- name: --version + type: string + short-summary: The version of the offer (optional, latest version will be used if not specified) +- name: --output-folder + type: string + short-summary: The folder path where the package will be downloaded +""" diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py index b259919fe82..d5fa03dade1 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py @@ -7,23 +7,37 @@ from azure.cli.core.commands.parameters import resource_group_name_type + def load_arguments(self, _): - - with self.argument_context('disconnectedoperations edgemarketplace listoffers') as c: - c.argument('resource_group_name', arg_type=resource_group_name_type) - c.argument('resource_name', type=str, help='Name of the resource to list offers for') + with self.argument_context( + "disconnectedoperations edgemarketplace listoffers" + ) as c: + c.argument("resource_group_name", arg_type=resource_group_name_type) + c.argument( + "resource_name", type=str, help="Name of the resource to list offers for" + ) - with self.argument_context('disconnectedoperations edgemarketplace getoffer') as c: - c.argument('resource_group_name', arg_type=resource_group_name_type) - c.argument('resource_name', type=str, help='Name of the resource to list offers for') - c.argument('offer_name', type=str, help='Name of the offer to retrieve') - c.argument('product_name', type=str, help='Name of the product to retrieve') + with self.argument_context("disconnectedoperations edgemarketplace getoffer") as c: + c.argument("resource_group_name", arg_type=resource_group_name_type) + c.argument( + "resource_name", type=str, help="Name of the resource to list offers for" + ) + c.argument("offer_name", type=str, help="Name of the offer") + c.argument("publisher_name", type=str, help="Name of the publisher") - with self.argument_context('disconnectedoperations edgemarketplace packageoffer') as c: - c.argument('resource_group_name', arg_type=resource_group_name_type) - c.argument('resource_name', type=str, help='Name of the resource to list offers for') - c.argument('publisher_name', type=str, help='Name of the publisher') - c.argument('offer_name', type=str, help='Name of the offer to package') - c.argument('sku', type=str, help='SKU of the product to retrieve') - c.argument('version', type=str, help='Version of the product to retrieve') - c.argument('output_folder', type=str, help='Drive and directory to save the package to. Example: E:\\ or D:\\packages\\') + with self.argument_context( + "disconnectedoperations edgemarketplace packageoffer" + ) as c: + c.argument("resource_group_name", arg_type=resource_group_name_type) + c.argument( + "resource_name", type=str, help="Name of the resource to list offers for" + ) + c.argument("publisher_name", type=str, help="Name of the publisher") + c.argument("offer_name", type=str, help="Name of the offer to package") + c.argument("sku", type=str, help="SKU of the product") + c.argument("version", type=str, help="Version of the product") + c.argument( + "output_folder", + type=str, + help="Drive and directory to save the package to. Example: E:\\ or D:\\packages\\", + ) diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py index 2bb27f47f1c..813a9943e62 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py @@ -6,64 +6,80 @@ # -------------------------------------------------------------------------------------------- -from azure.cli.core.commands import CliCommandType from collections import OrderedDict +from azure.cli.core.commands import CliCommandType + + def transform_offers_table(result): if not result: return result - + # Transform each row while preserving order transformed = [] for item in result: - row = OrderedDict([ - ('Publisher', item['Publisher']), - ('Offer', item['Offer']), - ('SKU', item['SKU']), - ('Version', item['Versions']), - ('OS_Type', item['OS_Type']) - ]) + row = OrderedDict( + [ + ("Publisher", item["Publisher"]), + ("Offer", item["Offer"]), + ("SKU", item["SKU"]), + ("Version", item["Versions"]), + ("OS_Type", item["OS_Type"]), + ] + ) transformed.append(row) - + return transformed + def transform_offer_table(result): if not result: return result - + # Transform each row while preserving order transformed = [] for item in result: # Format versions to be on separate lines if it's a list/array - versions = item['Versions'] + versions = item["Versions"] if isinstance(versions, str): # Split by comma if it's a comma-separated string - versions = [v.strip() for v in versions.split(',')] - + versions = [v.strip() for v in versions.split(",")] + if isinstance(versions, (list, tuple)): # Format each version on a new line, preserving the full format - formatted_versions = '\n'.join(str(v).strip() for v in versions) + formatted_versions = "\n".join(str(v).strip() for v in versions) else: formatted_versions = str(versions) - row = OrderedDict([ - ('Publisher', item['Publisher']), - ('Offer', item['Offer']), - ('SKU', item['SKU']), - ('Version', formatted_versions), - ('OS_Type', item['OS_Type']) - ]) + row = OrderedDict( + [ + ("Publisher", item["Publisher"]), + ("Offer", item["Offer"]), + ("SKU", item["SKU"]), + ("Version", formatted_versions), + ("OS_Type", item["OS_Type"]), + ] + ) transformed.append(row) - + return transformed + def load_command_table(self, _): custom_command_type = CliCommandType( - operations_tmpl='azure.cli.command_modules.disconnectedoperations.custom#{}' + operations_tmpl="azure.cli.command_modules.disconnectedoperations.custom#{}" ) - with self.command_group('disconnectedoperations edgemarketplace', custom_command_type=custom_command_type) as g: - g.custom_command('listoffers', 'list_offers', table_transformer=transform_offers_table) - g.custom_command('getoffer', 'get_offer', table_transformer=transform_offer_table) - g.custom_command('packageoffer', 'package_offer') - - return self.command_table \ No newline at end of file + with self.command_group( + "disconnectedoperations edgemarketplace", + custom_command_type=custom_command_type, + is_preview=True, + ) as g: + g.custom_command( + "listoffers", "list_offers", table_transformer=transform_offers_table + ) + g.custom_command( + "getoffer", "get_offer", table_transformer=transform_offer_table + ) + g.custom_command("packageoffer", "package_offer") + + return self.command_table diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py index 1f855182ef5..e6ee98baa1f 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py @@ -8,32 +8,39 @@ # pylint: disable=too-many-lines # pylint: disable=too-many-statements -from knack.log import get_logger +provider_namespace = "Microsoft.DataBoxEdge" +sub_provider = "Microsoft.EdgeMarketPlace" +api_version = "2023-08-01-preview" -logger = get_logger(__name__) def _get_management_endpoint(cli_ctx): """Helper function to determine management endpoint based on cloud configuration.""" # cloud = cli_ctx.cloud # return cloud.endpoints.resource_manager - return "brazilus.management.azure.com" # For testing purposes - -def package_offer(cmd, - resource_group_name, - resource_name, - publisher_name, - offer_name, - sku, - version, - output_folder): + return "brazilus.management.azure.com" # For testing purposes + + +def package_offer( + cmd, + resource_group_name, + resource_name, + publisher_name, + offer_name, + sku, + version, + output_folder, +): """Get details of a specific marketplace offer and download its logos.""" - import os import json + import os + import shutil + import requests + from knack.log import get_logger + from azure.cli.core.commands.client_factory import get_subscription_id from azure.cli.core.util import send_raw_request - from knack.log import get_logger # Use helper function if management_endpoint not explicitly provided management_endpoint = _get_management_endpoint(cmd.cli_ctx) @@ -41,294 +48,552 @@ def package_offer(cmd, # Get subscription ID from current context subscription_id = get_subscription_id(cmd.cli_ctx) - - provider_namespace = "Private.EdgeInternal" - sub_provider = "Microsoft.EdgeMarketPlace" - api_version = "2023-08-01-preview" # Construct URL with parameters url = ( f"https://{management_endpoint}" f"/subscriptions/{subscription_id}" f"/resourceGroups/{resource_group_name}" - f"/providers/Microsoft.DataBoxEdge/dataBoxEdgeDevices/{resource_name}" + f"/providers/{provider_namespace}/dataBoxEdgeDevices/{resource_name}" f"/providers/{sub_provider}/offers/{publisher_name}:{offer_name}" f"?api-version={api_version}" ) resource = "https://management.azure.com" - + try: - response = send_raw_request(cmd.cli_ctx, 'get', url, resource=resource) - + response = send_raw_request(cmd.cli_ctx, "get", url, resource=resource) + if response.status_code == 200: data = response.json() - offer_content = data.get('properties', {}).get('offerContent', {}) - icon_uris = offer_content.get('iconFileUris', {}) - + offer_content = data.get("properties", {}).get("offerContent", {}) + icon_uris = offer_content.get("iconFileUris", {}) # Download logos and metadata if output folder is specified if output_folder: - publisher_id = offer_content.get('offerPublisher', {}).get('publisherId', '') - offer_id = offer_content.get('offerId', '') - skus = data.get('properties', {}).get('marketplaceSkus', []) - - for sku in skus: - sku_id = sku.get('marketplaceSkuId', '') - versions = sku.get('marketplaceSkuVersions', []) - - # If version is specified, filter for that version, else take the latest - if version: - versions = [v for v in versions if v.get('name') == version] - else: - versions = versions[:1] # Take only the latest version - - if not versions: - logger.warning(f"No matching version found for SKU {sku_id}") + publisher_id = offer_content.get("offerPublisher", {}).get( + "publisherId", "" + ) + offer_id = offer_content.get("offerId", "") + skus = data.get("properties", {}).get("marketplaceSkus", []) + + for _sku in skus: + sku_id = _sku.get("marketplaceSkuId", "") + + if sku_id != sku: continue + else: + # Store the generation information + generation = _sku.get("generation") - for version in versions: - version_id = version.get('name') - - # Create base path for this version - base_path = os.path.join(output_folder, 'catalog_artifacts', - publisher_id, offer_id, sku_id) - version_level_path = os.path.join(base_path, version_id) - icon_path = os.path.join(base_path, 'icons') - - # Check if version directory exists and has content - if os.path.exists(version_level_path): - # Check if directory has any files - if os.path.exists(os.path.join(version_level_path, 'metadata.json')) or \ - any(os.scandir(version_level_path)): - error_message = f"Version directory already exists and contains files: {version_level_path}. Please delete the version folder in case you want to re-download the package." - logger.error(error_message) - return { - 'error': error_message, - 'status': 'failed', - 'path': version_level_path - } + # Get all versions for this SKU + versions = _sku.get("marketplaceSkuVersions", []) + + versions = [v for v in versions if v.get("name") == version] + + if not versions: + logger.warning( + f"No matching version found for SKU {sku_id}" + ) + return + + # print if version and generation are found + print(f"Found VM version: {versions[0].get('name')}") + print(f"VM Generation: {generation}") - os.makedirs(icon_path, exist_ok=True) - os.makedirs(version_level_path, exist_ok=True) - - # Save metadata.json - metadata_path = os.path.join(version_level_path, 'metadata.json') - metadata = { - 'name': data.get('name'), - 'publisher': offer_content.get('offerPublisher'), - 'offer_id': offer_content.get('offerId'), - 'summary': offer_content.get('summary'), - 'description': offer_content.get('description'), - 'sku': { - 'name': sku.get('displayName'), - 'id': sku.get('marketplaceSkuId'), - 'os_type': sku.get('operatingSystem'), - 'version': version - } + version_id = versions[0].get("name") + + # check if sku is not found + if not version_id: + logger.warning(f"No matching SKU found: {sku}") + return + + # Create base path for this version + base_path = os.path.join( + output_folder, + "catalog_artifacts", + publisher_id, + offer_id, + sku_id, + ) + version_level_path = os.path.join(base_path, version_id) + icon_path = os.path.join(base_path, "icons") + + # Check if version directory exists and has content + if os.path.exists(version_level_path): + try: + # Remove directory and all its contents + shutil.rmtree(version_level_path) + logger.info( + f"Cleaned up existing version directory: {version_level_path}" + ) + except Exception as e: + error_message = f"Failed to clean up version directory {version_level_path}: {str(e)}" + logger.error(error_message) + return { + "error": error_message, + "status": "failed", + "path": version_level_path, } - - with open(metadata_path, 'w', encoding='utf-8') as f: - json.dump(metadata, f, indent=2) - logger.info(f"Saved metadata to {metadata_path}") - - # Download icons - if icon_uris: - for size, uri in icon_uris.items(): - file_extension = 'png' - file_path = os.path.join(icon_path, f"{size}.{file_extension}") - - # Skip if icon already exists - if os.path.exists(file_path): - logger.info(f"Icon {size} already exists at {file_path}, skipping download") - continue - - try: - logo_response = requests.get(uri) - if logo_response.status_code == 200: - with open(file_path, 'wb') as f: - f.write(logo_response.content) - logger.info(f"Downloaded {size} logo to {file_path}") - else: - logger.error(f"Failed to download {size} logo: {logo_response.status_code}") - except Exception as e: - logger.error(f"Error downloading {size} logo: {str(e)}") - - print ("Metadata and icons downloaded successfully") - + + os.makedirs(icon_path, exist_ok=True) + os.makedirs(version_level_path, exist_ok=True) + + # Save metadata.json + metadata_path = os.path.join(version_level_path, "metadata.json") + # Save Api response as it is on metadata.json + metadata = data + + with open(metadata_path, "w", encoding="utf-8") as f: + json.dump(metadata, f, indent=2) + logger.info(f"Saved metadata to {metadata_path}") + + # Download icons + if icon_uris: + for size, uri in icon_uris.items(): + file_extension = "png" + file_path = os.path.join(icon_path, f"{size}.{file_extension}") + + # Skip if icon already exists + if os.path.exists(file_path): + logger.info( + f"Icon {size} already exists at {file_path}, skipping download" + ) + continue + + try: + logo_response = requests.get(uri) + if logo_response.status_code == 200: + with open(file_path, "wb") as f: + f.write(logo_response.content) + logger.info(f"Downloaded {size} logo to {file_path}") + else: + logger.error( + f"Failed to download {size} logo: {logo_response.status_code}" + ) + except Exception as e: + logger.error(f"Error downloading {size} logo: {str(e)}") + + print("Metadata and icons downloaded successfully") + else: error_message = f"Request failed with status code: {response.status_code}" logger.error(error_message) return { - 'error': error_message, - 'status': 'failed', - 'resource_group_name': resource_group_name, - 'response': response.text + "error": error_message, + "status": "failed", + "resource_group_name": resource_group_name, + "response": response.text, } - + except Exception as e: logger.error(f"Failed to retrieve offer: {str(e)}") return { - 'error': str(e), - 'status': 'failed', - 'resource_group_name': resource_group_name + "error": str(e), + "status": "failed", + "resource_group_name": resource_group_name, + } + + print("Offer details retrieved successfully. Proceeding to download VHD.") + # Downloading VM image + return download_vhd( + cmd, + resource_group_name, + resource_name, + publisher_name, + offer_name, + sku, + version, + generation, + version_level_path, + ) + + +def download_vhd( + cmd, + resource_group_name, + resource_name, + publisher_name, + offer_name, + sku, + version, + generation, + output_folder, +): + """Generate access token for VHD download.""" + import json + import os + import time + from datetime import datetime + + from knack.log import get_logger + + from azure.cli.core.commands.client_factory import get_subscription_id + from azure.cli.core.util import send_raw_request + + logger = get_logger(__name__) + management_endpoint = _get_management_endpoint(cmd.cli_ctx) + subscription_id = get_subscription_id(cmd.cli_ctx) + + # API endpoint construction + url = ( + f"https://{management_endpoint}" + f"/subscriptions/{subscription_id}" + f"/resourceGroups/{resource_group_name}" + f"/providers/{provider_namespace}/dataBoxEdgeDevices/{resource_name}" + f"/providers/Microsoft.EdgeMarketPlace/offers/{publisher_name}:{offer_name}" + f"/generateAccessToken?api-version=2023-08-01-preview" + ) + + # Request body + body = { + "edgeMarketPlaceRegion": "westus", + "hypervGeneration": generation, + "marketPlaceSku": sku, + "marketPlaceSkuVersion": version, + } + + try: + print("Generating access token for VHD download...") + response = send_raw_request( + cmd.cli_ctx, + "post", + url, + resource="https://management.azure.com", + body=json.dumps(body), + ) + + print("Checking status of VHD download URL generation...") + print(response) + + # Check if the request was successful + if response.status_code not in (200, 202): + error_message = f"Request failed with status code: {response.status_code}" + logger.error(error_message) + return { + "error": error_message, + "status": "failed", + "resource_group_name": resource_group_name, + "response": response.text, + } + + # parse headers + headers = response.headers + + # get async operation URL from headers + async_operation_url = headers.get("Azure-AsyncOperation") + + # hit async operation URL until "status" in response is "Succeeded" with exponential backoff + if async_operation_url: + max_retries = 10 + base_delay = 2 # seconds + timeout = 300 # 5 minutes timeout + start_time = datetime.now() + + print("Hitting async operation URL...") + for attempt in range(max_retries): + print(f"Attempt {attempt + 1} of {max_retries}...") + try: + # Calculate exponential backoff delay + delay = base_delay * (2**attempt) + + # Check if we've exceeded timeout + if (datetime.now() - start_time).total_seconds() > timeout: + logger.error("Operation timed out after 5 minutes") + return { + "error": "Operation timed out", + "status": "failed", + "resource_group_name": resource_group_name, + } + + # Get operation status + status_response = send_raw_request( + cmd.cli_ctx, + "get", + async_operation_url, + resource="https://management.azure.com", + ) + + if status_response.status_code in (200, 202): + status_data = status_response.json() + status = status_data.get("status", "").lower() + + print("Current status:", status) + + if status == "succeeded": + logger.info("VHD download URL generation succeeded") + print(status_response) + # Get the download URL from the response + requestId = status_data.get("properties", {}).get( + "requestId" + ) + + # Obtaining SAS token using request Id + if requestId: + print( + f"Fetched request Id for VHD Download: {requestId}" + ) + + # Obtaining SAS token using request Id + token_url = ( + f"https://{management_endpoint}" + f"/subscriptions/{subscription_id}" + f"/resourceGroups/{resource_group_name}" + f"/providers/{provider_namespace}/dataBoxEdgeDevices/{resource_name}" + f"/providers/Microsoft.EdgeMarketPlace/offers/{publisher_name}:{offer_name}" + f"/getAccessToken?api-version={api_version}" + ) + + token_body = {"requestId": requestId} + + token_response = send_raw_request( + cmd.cli_ctx, + "post", + token_url, + resource="https://management.azure.com", + body=json.dumps(token_body), + ) + + if token_response.status_code == 200: + token_data = token_response.json() + + # Generate azcopy command + download_url = token_data.get("accessToken") + # diskId = token_data.get("diskId") + + # Construct the azcopy command + command = f'azcopy copy "{download_url}" "{output_folder}" --check-md5 NoCheck' + + print(command) + print("Executing command...") + + # Execute the command + os.system(command) + print("Download completed successfully.") + return { + "status": "succeeded", + "message": "Download completed successfully.", + } + else: + logger.error( + f"Failed to get access token: {token_response.status_code}" + ) + return { + "error": f"Failed to get access token: {token_response.status_code}", + "status": "failed", + } + + else: + logger.error("Download URL not found in response") + return { + "error": "Download URL not found", + "status": "failed", + } + + elif status == "failed": + error_message = status_data.get("error", {}).get( + "message", "Unknown error" + ) + logger.error(f"Operation failed: {error_message}") + return {"error": error_message, "status": "failed"} + + else: # In progress + logger.info( + f"Operation in progress... (attempt {attempt + 1}/{max_retries})" + ) + time.sleep(delay) + continue + + else: + logger.error( + f"Failed to get operation status: {status_response.status_code}" + ) + return { + "error": f"Status check failed: {status_response.status_code}", + "status": "failed", + } + + except Exception as e: + logger.error(f"Error checking operation status: {str(e)}") + time.sleep(delay) + continue + + # If we've exhausted all retries + logger.error("Maximum retry attempts reached") + return {"error": "Maximum retry attempts reached", "status": "failed"} + + except Exception as e: + logger.error(f"Failed to generate access token: {str(e)}") + return { + "error": str(e), + "status": "failed", + "resource_group_name": resource_group_name, } + def list_offers(cmd, resource_group_name, resource_name): """List all offers for disconnected operations.""" + from knack.log import get_logger + from azure.cli.core.commands.client_factory import get_subscription_id from azure.cli.core.util import send_raw_request - from knack.log import get_logger logger = get_logger(__name__) management_endpoint = _get_management_endpoint(cmd.cli_ctx) - + # Get subscription ID from current context subscription_id = get_subscription_id(cmd.cli_ctx) - provider_namespace="Private.EdgeInternal" - sub_provider="Microsoft.EdgeMarketPlace" - api_version="2023-08-01-preview" - + # Construct URL with parameters url = ( f"https://{management_endpoint}" f"/subscriptions/{subscription_id}" f"/resourceGroups/{resource_group_name}" - f"/providers/Microsoft.DataBoxEdge/dataBoxEdgeDevices/{resource_name}" + f"/providers/{provider_namespace}/dataBoxEdgeDevices/{resource_name}" f"/providers/{sub_provider}/offers" f"?api-version={api_version}" ) # Define headers with resource for authentication headers = { - 'Content-Type': 'application/json', + "Content-Type": "application/json", } # Define the resource for authentication - resource = "https://management.azure.com" # Using standard Azure management endpoint - + resource = ( + "https://management.azure.com" # Using standard Azure management endpoint + ) + try: - response = send_raw_request(cmd.cli_ctx, 'get', url, resource=resource) - + response = send_raw_request(cmd.cli_ctx, "get", url, resource=resource) + if response.status_code == 200: data = response.json() result = [] - - for offer in data.get('value', []): - offer_content = offer.get('properties', {}).get('offerContent', {}) - skus = offer.get('properties', {}).get('marketplaceSkus', []) - + + for offer in data.get("value", []): + offer_content = offer.get("properties", {}).get("offerContent", {}) + skus = offer.get("properties", {}).get("marketplaceSkus", []) + for sku in skus: - versions = sku.get('marketplaceSkuVersions', [])[:] + versions = sku.get("marketplaceSkuVersions", [])[:] row = { - 'Publisher': offer_content.get('offerPublisher', {}).get('publisherId'), - 'Offer': offer_content.get('offerId'), - 'SKU': sku.get('marketplaceSkuId'), - 'Versions': f"{len(versions)} {'version' if len(versions) == 1 else 'versions'} available", - 'OS_Type': sku.get('operatingSystem', {}).get('type') + "Publisher": offer_content.get("offerPublisher", {}).get( + "publisherId" + ), + "Offer": offer_content.get("offerId"), + "SKU": sku.get("marketplaceSkuId"), + "Versions": f"{len(versions)} {'version' if len(versions) == 1 else 'versions'} available", + "OS_Type": sku.get("operatingSystem", {}).get("type"), } result.append(row) - + return result - + else: error_message = f"Request failed with status code: {response.status_code}" logger.error(error_message) return { - 'error': error_message, - 'status': 'failed', - 'resource_group_name': resource_group_name, - 'response': response.text + "error": error_message, + "status": "failed", + "resource_group_name": resource_group_name, + "response": response.text, } - + except Exception as e: logger.error(f"Failed to retrieve offers: {str(e)}") return { - 'error': str(e), - 'status': 'failed', - 'resource_group_name': resource_group_name + "error": str(e), + "status": "failed", + "resource_group_name": resource_group_name, } - + + def get_offer(cmd, resource_group_name, resource_name, publisher_name, offer_name): """List all offers for disconnected operations.""" + from knack.log import get_logger + from azure.cli.core.commands.client_factory import get_subscription_id from azure.cli.core.util import send_raw_request - from knack.log import get_logger logger = get_logger(__name__) management_endpoint = _get_management_endpoint(cmd.cli_ctx) - + # Get subscription ID from current context subscription_id = get_subscription_id(cmd.cli_ctx) - provider_namespace="Private.EdgeInternal" - sub_provider="Microsoft.EdgeMarketPlace" - api_version="2023-08-01-preview" - + # Construct URL with parameters url = ( f"https://{management_endpoint}" f"/subscriptions/{subscription_id}" f"/resourceGroups/{resource_group_name}" - f"/providers/Microsoft.DataBoxEdge/dataBoxEdgeDevices/{resource_name}" + f"/providers/{provider_namespace}/dataBoxEdgeDevices/{resource_name}" f"/providers/{sub_provider}/offers/{publisher_name}:{offer_name}" f"?api-version={api_version}" ) # Define headers with resource for authentication headers = { - 'Content-Type': 'application/json', + "Content-Type": "application/json", } # Define the resource for authentication - resource = "https://management.azure.com" # Using standard Azure management endpoint - + resource = ( + "https://management.azure.com" # Using standard Azure management endpoint + ) + try: - response = send_raw_request(cmd.cli_ctx, 'get', url, resource=resource) - + response = send_raw_request(cmd.cli_ctx, "get", url, resource=resource) + if response.status_code == 200: data = response.json() result = [] - - offer_content = data.get('properties', {}).get('offerContent', {}) - skus = data.get('properties', {}).get('marketplaceSkus', []) + offer_content = data.get("properties", {}).get("offerContent", {}) + skus = data.get("properties", {}).get("marketplaceSkus", []) for sku in skus: # Get all versions for this SKU - versions = sku.get('marketplaceSkuVersions', [])[:] + versions = sku.get("marketplaceSkuVersions", [])[:] # transform versions and size array into a multi-line string - version_str = ', '.join([f"{v.get('name')}({v.get('minimumDownloadSizeInMb')}MB)" - for v in versions]) - + version_str = ", ".join( + [ + f"{v.get('name')}({v.get('minimumDownloadSizeInMb')}MB)" + for v in versions + ] + ) + # Create a single row with flattened version info row = { - 'Publisher': offer_content.get('offerPublisher', {}).get('publisherId'), - 'Offer': offer_content.get('offerId'), - 'SKU': sku.get('marketplaceSkuId'), - 'Versions': version_str, - 'OS_Type': sku.get('operatingSystem', {}).get('type') + "Publisher": offer_content.get("offerPublisher", {}).get( + "publisherId" + ), + "Offer": offer_content.get("offerId"), + "SKU": sku.get("marketplaceSkuId"), + "Versions": version_str, + "OS_Type": sku.get("operatingSystem", {}).get("type"), } result.append(row) return result - + else: error_message = f"Request failed with status code: {response.status_code}" logger.error(error_message) return { - 'error': error_message, - 'status': 'failed', - 'resource_group_name': resource_group_name, - 'response': response.text + "error": error_message, + "status": "failed", + "resource_group_name": resource_group_name, + "response": response.text, } - + except Exception as e: logger.error(f"Failed to retrieve offers: {str(e)}") return { - 'error': str(e), - 'status': 'failed', - 'resource_group_name': resource_group_name - } \ No newline at end of file + "error": str(e), + "status": "failed", + "resource_group_name": resource_group_name, + } From 4e310f15cfec882b2e8e6cc165e668821639c44c Mon Sep 17 00:00:00 2001 From: Sankalp Kotewar Date: Tue, 25 Feb 2025 18:21:37 +0530 Subject: [PATCH 08/24] removing mgmt storage latest --- src/azure-cli-core/azure/cli/core/profiles/_shared.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/azure-cli-core/azure/cli/core/profiles/_shared.py b/src/azure-cli-core/azure/cli/core/profiles/_shared.py index 44e161f1b2d..c42d311d068 100644 --- a/src/azure-cli-core/azure/cli/core/profiles/_shared.py +++ b/src/azure-cli-core/azure/cli/core/profiles/_shared.py @@ -157,7 +157,7 @@ def default_api_version(self): AZURE_API_PROFILES = { 'latest': { ResourceType.MGMT_DISCONNECTEDOPERATIONS: '2024-12-01-preview', - ResourceType.MGMT_STORAGE: '2024-01-01', + #ResourceType.MGMT_STORAGE: '2024-01-01', ResourceType.MGMT_NETWORK: '2022-01-01', ResourceType.MGMT_COMPUTE: SDKProfile('2024-07-01', { 'resource_skus': '2019-04-01', From f888c184a0e6ab2cb553680f402ddb93c5d42bcb Mon Sep 17 00:00:00 2001 From: Sankalp Kotewar Date: Tue, 25 Feb 2025 18:27:41 +0530 Subject: [PATCH 09/24] Added storage mgmt version back --- src/azure-cli-core/azure/cli/core/profiles/_shared.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/azure-cli-core/azure/cli/core/profiles/_shared.py b/src/azure-cli-core/azure/cli/core/profiles/_shared.py index c42d311d068..44e161f1b2d 100644 --- a/src/azure-cli-core/azure/cli/core/profiles/_shared.py +++ b/src/azure-cli-core/azure/cli/core/profiles/_shared.py @@ -157,7 +157,7 @@ def default_api_version(self): AZURE_API_PROFILES = { 'latest': { ResourceType.MGMT_DISCONNECTEDOPERATIONS: '2024-12-01-preview', - #ResourceType.MGMT_STORAGE: '2024-01-01', + ResourceType.MGMT_STORAGE: '2024-01-01', ResourceType.MGMT_NETWORK: '2022-01-01', ResourceType.MGMT_COMPUTE: SDKProfile('2024-07-01', { 'resource_skus': '2019-04-01', From abe306cf4cb059c9e1c496a064075a2c55e03eb7 Mon Sep 17 00:00:00 2001 From: Sankalp Kotewar Date: Wed, 12 Feb 2025 08:59:12 +0530 Subject: [PATCH 10/24] initial commit --- .../azure/cli/core/profiles/_shared.py | 4 +- .../disconnectedoperations/__init__.py | 35 ++ .../disconnectedoperations/_client_factory.py | 21 + .../disconnectedoperations/_help.py | 73 ++++ .../disconnectedoperations/_params.py | 30 ++ .../disconnectedoperations/aaz/__init__.py | 6 + .../aaz/latest/__init__.py | 10 + .../aaz/latest/edge/__cmd_group.py | 24 ++ .../aaz/latest/edge/__init__.py | 11 + .../disconnected_operation/__cmd_group.py | 24 ++ .../edge/disconnected_operation/__init__.py | 12 + .../edge/disconnected_operation/_list.py | 396 ++++++++++++++++++ .../image/__cmd_group.py | 24 ++ .../disconnected_operation/image/__init__.py | 12 + .../image/_list_download_uri.py | 221 ++++++++++ .../disconnectedoperations/commands.py | 27 ++ .../disconnectedoperations/custom.py | 345 +++++++++++++++ .../disconnectedoperations/tests/__init__.py | 6 + .../tests/latest/__init__.py | 6 + .../latest/test_disconnectedoperations.py | 24 ++ 20 files changed, 1310 insertions(+), 1 deletion(-) create mode 100644 src/azure-cli/azure/cli/command_modules/disconnectedoperations/__init__.py create mode 100644 src/azure-cli/azure/cli/command_modules/disconnectedoperations/_client_factory.py create mode 100644 src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py create mode 100644 src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py create mode 100644 src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/__init__.py create mode 100644 src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/__init__.py create mode 100644 src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/__cmd_group.py create mode 100644 src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/__init__.py create mode 100644 src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/__cmd_group.py create mode 100644 src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/__init__.py create mode 100644 src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/_list.py create mode 100644 src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/image/__cmd_group.py create mode 100644 src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/image/__init__.py create mode 100644 src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/image/_list_download_uri.py create mode 100644 src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py create mode 100644 src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py create mode 100644 src/azure-cli/azure/cli/command_modules/disconnectedoperations/tests/__init__.py create mode 100644 src/azure-cli/azure/cli/command_modules/disconnectedoperations/tests/latest/__init__.py create mode 100644 src/azure-cli/azure/cli/command_modules/disconnectedoperations/tests/latest/test_disconnectedoperations.py diff --git a/src/azure-cli-core/azure/cli/core/profiles/_shared.py b/src/azure-cli-core/azure/cli/core/profiles/_shared.py index 1c72649519b..44e161f1b2d 100644 --- a/src/azure-cli-core/azure/cli/core/profiles/_shared.py +++ b/src/azure-cli-core/azure/cli/core/profiles/_shared.py @@ -10,7 +10,6 @@ from knack.log import get_logger - logger = get_logger(__name__) @@ -84,6 +83,8 @@ class ResourceType(Enum): # pylint: disable=too-few-public-methods MGMT_CUSTOMLOCATION = ('azure.mgmt.extendedlocation', 'CustomLocations') MGMT_CONTAINERSERVICE = ('azure.mgmt.containerservice', 'ContainerServiceClient') MGMT_APPCONTAINERS = ('azure.mgmt.appcontainers', 'ContainerAppsAPIClient') + MGMT_DISCONNECTEDOPERATIONS = ('azure.mgmt.disconnectedoperations', 'DisconnectedOperationsClient') + # the "None" below will stay till a command module fills in the type so "get_mgmt_service_client" # can be provided with "ResourceType.XXX" to initialize the client object. This usually happens @@ -155,6 +156,7 @@ def default_api_version(self): AZURE_API_PROFILES = { 'latest': { + ResourceType.MGMT_DISCONNECTEDOPERATIONS: '2024-12-01-preview', ResourceType.MGMT_STORAGE: '2024-01-01', ResourceType.MGMT_NETWORK: '2022-01-01', ResourceType.MGMT_COMPUTE: SDKProfile('2024-07-01', { diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/__init__.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/__init__.py new file mode 100644 index 00000000000..1aaa566929a --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/__init__.py @@ -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. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- + +from azure.cli.core import AzCommandsLoader +from azure.cli.command_modules.disconnectedoperations._help import helps # pylint: disable=unused-import +from azure.cli.command_modules.disconnectedoperations._client_factory import cf_image + + +class DisconnectedoperationsCommandsLoader(AzCommandsLoader): + + def __init__(self, cli_ctx=None): + from azure.cli.core.commands import CliCommandType + from azure.cli.core.profiles import ResourceType # required when using python sdk + disconnectedoperations_custom = CliCommandType( + operations_tmpl='azure.cli.command_modules.disconnectedoperations.custom#{}', + client_factory=cf_image) + super(DisconnectedoperationsCommandsLoader, self).__init__(cli_ctx=cli_ctx, + resource_type=ResourceType.MGMT_DISCONNECTEDOPERATIONS, # required when using python sdk + custom_command_type=disconnectedoperations_custom) + + def load_command_table(self, args): + from azure.cli.command_modules.disconnectedoperations.commands import load_command_table + load_command_table(self, args) + return self.command_table + + def load_arguments(self, command): + from azure.cli.command_modules.disconnectedoperations._params import load_arguments + load_arguments(self, command) + + +COMMAND_LOADER_CLS = DisconnectedoperationsCommandsLoader diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_client_factory.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_client_factory.py new file mode 100644 index 00000000000..20df0404d3b --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_client_factory.py @@ -0,0 +1,21 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + + +def get_disconnectedoperations_management_client(cli_ctx, *_): + from azure.cli.core.commands.client_factory import get_mgmt_service_client + from azure.mgmt.disconnectedoperations import DisconnectedOperationsClient + return get_mgmt_service_client(cli_ctx, DisconnectedOperationsClient) + + +def cf_image(cli_ctx, *_): + return get_disconnectedoperations_management_client(cli_ctx).image + +def cf_logos(cli_ctx, *_): + return get_disconnectedoperations_management_client(cli_ctx).logos + +def cf_metadata(cli_ctx, *_): + return get_disconnectedoperations_management_client(cli_ctx).metadata + diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py new file mode 100644 index 00000000000..492066f4645 --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py @@ -0,0 +1,73 @@ +from knack.help_files import helps # pylint: disable=unused-import + +helps['disconnectedoperations'] = """ + type: group + short-summary: Commands to manage disconnected operations. + long-summary: Manage Azure Edge marketplace operations in disconnected environments. +""" + +helps['disconnectedoperations edgemarketplace'] = """ + type: group + short-summary: Manage Edge Marketplace operations. + long-summary: Commands to manage Edge Marketplace images and offers. +""" + +helps['disconnectedoperations edgemarketplace listoffers'] = """ + type: command + short-summary: List available marketplace offers. + long-summary: List all available marketplace offers with their SKUs and versions. + parameters: + - name: --resource-group -g + type: string + required: true + short-summary: Name of resource group. + - name: --management-endpoint + type: string + short-summary: Management endpoint URL. + default: brazilus.management.azure.com + - name: --provider-namespace + type: string + short-summary: Provider namespace. + default: Private.EdgeInternal + - name: --api-version + type: string + short-summary: API version to use. + default: 2023-08-01-preview + examples: + - name: List offers in default format + text: az disconnectedoperations edgemarketplace listoffers -g myResourceGroup + - name: List offers in table format + text: az disconnectedoperations edgemarketplace listoffers -g myResourceGroup --output table + - name: List offers with custom endpoint + text: az disconnectedoperations edgemarketplace listoffers -g myResourceGroup --management-endpoint customendpoint.azure.com +""" + +helps['disconnectedoperations edgemarketplace packageimage'] = """ + type: command + short-summary: Package a marketplace image. + long-summary: Download and package a marketplace image for use in disconnected environments. + parameters: + - name: --resource-group -g + type: string + required: true + short-summary: Name of resource group. + - name: --publisher + type: string + required: true + short-summary: Publisher of the marketplace image. + - name: --offer + type: string + required: true + short-summary: Offer name of the marketplace image. + - name: --sku + type: string + required: true + short-summary: SKU of the marketplace image. + - name: --location -l + type: string + required: true + short-summary: Location for the packaged image. + examples: + - name: Package a Windows Server image + text: az disconnectedoperations edgemarketplace packageimage -g myResourceGroup --publisher MicrosoftWindowsServer --offer WindowsServer --sku 2019-Datacenter --location eastus +""" \ No newline at end of file diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py new file mode 100644 index 00000000000..ec6554388c5 --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py @@ -0,0 +1,30 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- + +# pylint: disable=too-many-lines +# pylint: disable=too-many-statements + +from azure.cli.core.commands.parameters import resource_group_name_type + + +def load_arguments(self, _): # pylint: disable=unused-argument + with self.argument_context('disconnectedoperations edgemarketplace packageimage') as c: + c.argument('resource_group_name', arg_type=resource_group_name_type) + c.argument('publisher', options_list=['--publisher']) + c.argument('offer', options_list=['--offer']) + c.argument('sku', options_list=['--skus']) + c.argument('location', options_list=['--location']) + + with self.argument_context('disconnectedoperations edgemarketplace listoffers') as c: + c.argument('management_endpoint', type=str, + help='Management endpoint URL') + c.argument('provider_namespace', type=str, + help='Provider namespace') + c.argument('sub_provider', type=str, + help='Sub-provider namespace') + c.argument('api_version', type=str, + help='API version') diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/__init__.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/__init__.py new file mode 100644 index 00000000000..5757aea3175 --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/__init__.py @@ -0,0 +1,6 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/__init__.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/__init__.py new file mode 100644 index 00000000000..f6acc11aa4e --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/__init__.py @@ -0,0 +1,10 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- + +# pylint: skip-file +# flake8: noqa + diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/__cmd_group.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/__cmd_group.py new file mode 100644 index 00000000000..30f0e46625f --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/__cmd_group.py @@ -0,0 +1,24 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- + +# pylint: skip-file +# flake8: noqa + +from azure.cli.core.aaz import * + + +@register_command_group( + "edge", + is_preview=True, +) +class __CMDGroup(AAZCommandGroup): + """Edge disconnected operations CLI + """ + pass + + +__all__ = ["__CMDGroup"] diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/__init__.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/__init__.py new file mode 100644 index 00000000000..5a9d61963d6 --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/__init__.py @@ -0,0 +1,11 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- + +# pylint: skip-file +# flake8: noqa + +from .__cmd_group import * diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/__cmd_group.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/__cmd_group.py new file mode 100644 index 00000000000..fce13eff9a7 --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/__cmd_group.py @@ -0,0 +1,24 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- + +# pylint: skip-file +# flake8: noqa + +from azure.cli.core.aaz import * + + +@register_command_group( + "edge disconnected-operation", + is_preview=True, +) +class __CMDGroup(AAZCommandGroup): + """Disconnected operations cli + """ + pass + + +__all__ = ["__CMDGroup"] diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/__init__.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/__init__.py new file mode 100644 index 00000000000..d63ae5a6fc9 --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/__init__.py @@ -0,0 +1,12 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- + +# pylint: skip-file +# flake8: noqa + +from .__cmd_group import * +from ._list import * diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/_list.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/_list.py new file mode 100644 index 00000000000..45101687c88 --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/_list.py @@ -0,0 +1,396 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- + +# pylint: skip-file +# flake8: noqa + +from azure.cli.core.aaz import * + + +@register_command( + "edge disconnected-operation list", + is_preview=True, +) +class List(AAZCommand): + """List DisconnectedOperation resources + + List DisconnectedOperation resources by subscription ID and resource group + """ + + _aaz_info = { + "version": "2024-12-01-preview", + "resources": [ + ["mgmt-plane", "/subscriptions/{}/providers/microsoft.edge/disconnectedoperations", "2024-12-01-preview"], + ["mgmt-plane", "/subscriptions/{}/resourcegroups/{}/providers/microsoft.edge/disconnectedoperations", "2024-12-01-preview"], + ] + } + + AZ_SUPPORT_PAGINATION = True + + def _handler(self, command_args): + super()._handler(command_args) + return self.build_paging(self._execute_operations, self._output) + + _args_schema = None + + @classmethod + def _build_arguments_schema(cls, *args, **kwargs): + if cls._args_schema is not None: + return cls._args_schema + cls._args_schema = super()._build_arguments_schema(*args, **kwargs) + + # define Arg Group "" + + _args_schema = cls._args_schema + _args_schema.resource_group = AAZResourceGroupNameArg() + return cls._args_schema + + def _execute_operations(self): + self.pre_operations() + condition_0 = has_value(self.ctx.subscription_id) and has_value(self.ctx.args.resource_group) is not True + condition_1 = has_value(self.ctx.args.resource_group) and has_value(self.ctx.subscription_id) + if condition_0: + self.DisconnectedOperationsListBySubscription(ctx=self.ctx)() + if condition_1: + self.DisconnectedOperationsListByResourceGroup(ctx=self.ctx)() + self.post_operations() + + @register_callback + def pre_operations(self): + pass + + @register_callback + def post_operations(self): + pass + + def _output(self, *args, **kwargs): + result = self.deserialize_output(self.ctx.vars.instance.value, client_flatten=True) + next_link = self.deserialize_output(self.ctx.vars.instance.next_link) + return result, next_link + + class DisconnectedOperationsListBySubscription(AAZHttpOperation): + CLIENT_TYPE = "MgmtClient" + + def __call__(self, *args, **kwargs): + request = self.make_request() + session = self.client.send_request(request=request, stream=False, **kwargs) + if session.http_response.status_code in [200]: + return self.on_200(session) + + return self.on_error(session.http_response) + + @property + def url(self): + return self.client.format_url( + "/subscriptions/{subscriptionId}/providers/Microsoft.Edge/disconnectedOperations", + **self.url_parameters + ) + + @property + def method(self): + return "GET" + + @property + def error_format(self): + return "MgmtErrorFormat" + + @property + def url_parameters(self): + parameters = { + **self.serialize_url_param( + "subscriptionId", self.ctx.subscription_id, + required=True, + ), + } + return parameters + + @property + def query_parameters(self): + parameters = { + **self.serialize_query_param( + "api-version", "2024-12-01-preview", + required=True, + ), + } + return parameters + + @property + def header_parameters(self): + parameters = { + **self.serialize_header_param( + "Accept", "application/json", + ), + } + return parameters + + def on_200(self, session): + data = self.deserialize_http_content(session) + self.ctx.set_var( + "instance", + data, + schema_builder=self._build_schema_on_200 + ) + + _schema_on_200 = None + + @classmethod + def _build_schema_on_200(cls): + if cls._schema_on_200 is not None: + return cls._schema_on_200 + + cls._schema_on_200 = AAZObjectType() + + _schema_on_200 = cls._schema_on_200 + _schema_on_200.next_link = AAZStrType( + serialized_name="nextLink", + ) + _schema_on_200.value = AAZListType( + flags={"required": True}, + ) + + value = cls._schema_on_200.value + value.Element = AAZObjectType() + + _element = cls._schema_on_200.value.Element + _element.id = AAZStrType( + flags={"read_only": True}, + ) + _element.location = AAZStrType( + flags={"required": True}, + ) + _element.name = AAZStrType( + flags={"read_only": True}, + ) + _element.properties = AAZObjectType() + _element.system_data = AAZObjectType( + serialized_name="systemData", + flags={"read_only": True}, + ) + _element.tags = AAZDictType() + _element.type = AAZStrType( + flags={"read_only": True}, + ) + + properties = cls._schema_on_200.value.Element.properties + properties.billing_model = AAZStrType( + serialized_name="billingModel", + flags={"read_only": True}, + ) + properties.connection_intent = AAZStrType( + serialized_name="connectionIntent", + flags={"required": True}, + ) + properties.connection_status = AAZStrType( + serialized_name="connectionStatus", + flags={"read_only": True}, + ) + properties.device_version = AAZStrType( + serialized_name="deviceVersion", + ) + properties.provisioning_state = AAZStrType( + serialized_name="provisioningState", + flags={"read_only": True}, + ) + properties.registration_status = AAZStrType( + serialized_name="registrationStatus", + ) + properties.stamp_id = AAZStrType( + serialized_name="stampId", + flags={"read_only": True}, + ) + + system_data = cls._schema_on_200.value.Element.system_data + system_data.created_at = AAZStrType( + serialized_name="createdAt", + ) + system_data.created_by = AAZStrType( + serialized_name="createdBy", + ) + system_data.created_by_type = AAZStrType( + serialized_name="createdByType", + ) + system_data.last_modified_at = AAZStrType( + serialized_name="lastModifiedAt", + ) + system_data.last_modified_by = AAZStrType( + serialized_name="lastModifiedBy", + ) + system_data.last_modified_by_type = AAZStrType( + serialized_name="lastModifiedByType", + ) + + tags = cls._schema_on_200.value.Element.tags + tags.Element = AAZStrType() + + return cls._schema_on_200 + + class DisconnectedOperationsListByResourceGroup(AAZHttpOperation): + CLIENT_TYPE = "MgmtClient" + + def __call__(self, *args, **kwargs): + request = self.make_request() + session = self.client.send_request(request=request, stream=False, **kwargs) + if session.http_response.status_code in [200]: + return self.on_200(session) + + return self.on_error(session.http_response) + + @property + def url(self): + return self.client.format_url( + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Edge/disconnectedOperations", + **self.url_parameters + ) + + @property + def method(self): + return "GET" + + @property + def error_format(self): + return "MgmtErrorFormat" + + @property + def url_parameters(self): + parameters = { + **self.serialize_url_param( + "resourceGroupName", self.ctx.args.resource_group, + required=True, + ), + **self.serialize_url_param( + "subscriptionId", self.ctx.subscription_id, + required=True, + ), + } + return parameters + + @property + def query_parameters(self): + parameters = { + **self.serialize_query_param( + "api-version", "2024-12-01-preview", + required=True, + ), + } + return parameters + + @property + def header_parameters(self): + parameters = { + **self.serialize_header_param( + "Accept", "application/json", + ), + } + return parameters + + def on_200(self, session): + data = self.deserialize_http_content(session) + self.ctx.set_var( + "instance", + data, + schema_builder=self._build_schema_on_200 + ) + + _schema_on_200 = None + + @classmethod + def _build_schema_on_200(cls): + if cls._schema_on_200 is not None: + return cls._schema_on_200 + + cls._schema_on_200 = AAZObjectType() + + _schema_on_200 = cls._schema_on_200 + _schema_on_200.next_link = AAZStrType( + serialized_name="nextLink", + ) + _schema_on_200.value = AAZListType( + flags={"required": True}, + ) + + value = cls._schema_on_200.value + value.Element = AAZObjectType() + + _element = cls._schema_on_200.value.Element + _element.id = AAZStrType( + flags={"read_only": True}, + ) + _element.location = AAZStrType( + flags={"required": True}, + ) + _element.name = AAZStrType( + flags={"read_only": True}, + ) + _element.properties = AAZObjectType() + _element.system_data = AAZObjectType( + serialized_name="systemData", + flags={"read_only": True}, + ) + _element.tags = AAZDictType() + _element.type = AAZStrType( + flags={"read_only": True}, + ) + + properties = cls._schema_on_200.value.Element.properties + properties.billing_model = AAZStrType( + serialized_name="billingModel", + flags={"read_only": True}, + ) + properties.connection_intent = AAZStrType( + serialized_name="connectionIntent", + flags={"required": True}, + ) + properties.connection_status = AAZStrType( + serialized_name="connectionStatus", + flags={"read_only": True}, + ) + properties.device_version = AAZStrType( + serialized_name="deviceVersion", + ) + properties.provisioning_state = AAZStrType( + serialized_name="provisioningState", + flags={"read_only": True}, + ) + properties.registration_status = AAZStrType( + serialized_name="registrationStatus", + ) + properties.stamp_id = AAZStrType( + serialized_name="stampId", + flags={"read_only": True}, + ) + + system_data = cls._schema_on_200.value.Element.system_data + system_data.created_at = AAZStrType( + serialized_name="createdAt", + ) + system_data.created_by = AAZStrType( + serialized_name="createdBy", + ) + system_data.created_by_type = AAZStrType( + serialized_name="createdByType", + ) + system_data.last_modified_at = AAZStrType( + serialized_name="lastModifiedAt", + ) + system_data.last_modified_by = AAZStrType( + serialized_name="lastModifiedBy", + ) + system_data.last_modified_by_type = AAZStrType( + serialized_name="lastModifiedByType", + ) + + tags = cls._schema_on_200.value.Element.tags + tags.Element = AAZStrType() + + return cls._schema_on_200 + + +class _ListHelper: + """Helper class for List""" + + +__all__ = ["List"] diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/image/__cmd_group.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/image/__cmd_group.py new file mode 100644 index 00000000000..79a62e83275 --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/image/__cmd_group.py @@ -0,0 +1,24 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- + +# pylint: skip-file +# flake8: noqa + +from azure.cli.core.aaz import * + + +@register_command_group( + "edge disconnected-operation image", + is_preview=True, +) +class __CMDGroup(AAZCommandGroup): + """Disconnected operations image CLI + """ + pass + + +__all__ = ["__CMDGroup"] diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/image/__init__.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/image/__init__.py new file mode 100644 index 00000000000..5e75ed17830 --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/image/__init__.py @@ -0,0 +1,12 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- + +# pylint: skip-file +# flake8: noqa + +from .__cmd_group import * +from ._list_download_uri import * diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/image/_list_download_uri.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/image/_list_download_uri.py new file mode 100644 index 00000000000..2629febe4e2 --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/aaz/latest/edge/disconnected_operation/image/_list_download_uri.py @@ -0,0 +1,221 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- + +# pylint: skip-file +# flake8: noqa + +from azure.cli.core.aaz import * + + +@register_command( + "edge disconnected-operation image list-download-uri", + is_preview=True, +) +class ListDownloadUri(AAZCommand): + """Get deployment manifest. + """ + + _aaz_info = { + "version": "2024-12-01-preview", + "resources": [ + ["mgmt-plane", "/subscriptions/{}/resourcegroups/{}/providers/microsoft.edge/disconnectedoperations/{}/images/{}/listdownloaduri", "2024-12-01-preview"], + ] + } + + def _handler(self, command_args): + super()._handler(command_args) + self._execute_operations() + return self._output() + + _args_schema = None + + @classmethod + def _build_arguments_schema(cls, *args, **kwargs): + if cls._args_schema is not None: + return cls._args_schema + cls._args_schema = super()._build_arguments_schema(*args, **kwargs) + + # define Arg Group "" + + _args_schema = cls._args_schema + _args_schema.image_name = AAZStrArg( + options=["--image-name"], + help="The name of the Image", + required=True, + id_part="child_name_1", + fmt=AAZStrArgFormat( + pattern="^[a-zA-Z0-9-]{3,24}$", + ), + ) + _args_schema.name = AAZStrArg( + options=["--name"], + help="Name of the resource", + required=True, + id_part="name", + fmt=AAZStrArgFormat( + pattern="^[a-zA-Z0-9][a-zA-Z0-9-_]{2,22}[a-zA-Z0-9]$", + ), + ) + _args_schema.resource_group = AAZResourceGroupNameArg( + required=True, + ) + return cls._args_schema + + def _execute_operations(self): + self.pre_operations() + self.ImagesListDownloadUri(ctx=self.ctx)() + self.post_operations() + + @register_callback + def pre_operations(self): + pass + + @register_callback + def post_operations(self): + pass + + def _output(self, *args, **kwargs): + result = self.deserialize_output(self.ctx.vars.instance, client_flatten=True) + return result + + class ImagesListDownloadUri(AAZHttpOperation): + CLIENT_TYPE = "MgmtClient" + + def __call__(self, *args, **kwargs): + request = self.make_request() + session = self.client.send_request(request=request, stream=False, **kwargs) + if session.http_response.status_code in [200]: + return self.on_200(session) + + return self.on_error(session.http_response) + + @property + def url(self): + return self.client.format_url( + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Edge/disconnectedOperations/{name}/images/{imageName}/listDownloadUri", + **self.url_parameters + ) + + @property + def method(self): + return "POST" + + @property + def error_format(self): + return "MgmtErrorFormat" + + @property + def url_parameters(self): + parameters = { + **self.serialize_url_param( + "imageName", self.ctx.args.image_name, + required=True, + ), + **self.serialize_url_param( + "name", self.ctx.args.name, + required=True, + ), + **self.serialize_url_param( + "resourceGroupName", self.ctx.args.resource_group, + required=True, + ), + **self.serialize_url_param( + "subscriptionId", self.ctx.subscription_id, + required=True, + ), + } + return parameters + + @property + def query_parameters(self): + parameters = { + **self.serialize_query_param( + "api-version", "2024-12-01-preview", + required=True, + ), + } + return parameters + + @property + def header_parameters(self): + parameters = { + **self.serialize_header_param( + "Accept", "application/json", + ), + } + return parameters + + def on_200(self, session): + data = self.deserialize_http_content(session) + self.ctx.set_var( + "instance", + data, + schema_builder=self._build_schema_on_200 + ) + + _schema_on_200 = None + + @classmethod + def _build_schema_on_200(cls): + if cls._schema_on_200 is not None: + return cls._schema_on_200 + + cls._schema_on_200 = AAZObjectType() + + _schema_on_200 = cls._schema_on_200 + _schema_on_200.compatible_versions = AAZListType( + serialized_name="compatibleVersions", + flags={"read_only": True}, + ) + _schema_on_200.download_link = AAZStrType( + serialized_name="downloadLink", + flags={"read_only": True}, + ) + _schema_on_200.link_expiry = AAZStrType( + serialized_name="linkExpiry", + flags={"read_only": True}, + ) + _schema_on_200.provisioning_state = AAZStrType( + serialized_name="provisioningState", + flags={"read_only": True}, + ) + _schema_on_200.release_date = AAZStrType( + serialized_name="releaseDate", + flags={"read_only": True}, + ) + _schema_on_200.release_display_name = AAZStrType( + serialized_name="releaseDisplayName", + flags={"read_only": True}, + ) + _schema_on_200.release_notes = AAZStrType( + serialized_name="releaseNotes", + flags={"read_only": True}, + ) + _schema_on_200.release_type = AAZStrType( + serialized_name="releaseType", + flags={"read_only": True}, + ) + _schema_on_200.release_version = AAZStrType( + serialized_name="releaseVersion", + flags={"read_only": True}, + ) + _schema_on_200.transaction_id = AAZStrType( + serialized_name="transactionId", + flags={"read_only": True}, + ) + + compatible_versions = cls._schema_on_200.compatible_versions + compatible_versions.Element = AAZStrType() + + return cls._schema_on_200 + + +class _ListDownloadUriHelper: + """Helper class for ListDownloadUri""" + + +__all__ = ["ListDownloadUri"] diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py new file mode 100644 index 00000000000..cc842ac3e1b --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py @@ -0,0 +1,27 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- + +# pylint: disable=too-many-lines +# pylint: disable=too-many-statements + +# from azure.cli.core.commands import CliCommandType +# from azure.cli.core.profiles import ResourceType + +from azure.cli.core.commands import CliCommandType + +def load_command_table(self, _): + custom_command_type = CliCommandType( + operations_tmpl='azure.cli.command_modules.disconnectedoperations.custom#{}' + ) + + with self.command_group('disconnectedoperations edgemarketplace', custom_command_type=custom_command_type) as g: + g.custom_command('packageimage', 'package_image') + g.custom_command('listoffers', 'list_offers') + g.custom_command('get-download-url', 'get_image_download_url') + g.custom_command('getoffer', 'get_offer') + + return self.command_table \ No newline at end of file diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py new file mode 100644 index 00000000000..77dd8b3b112 --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py @@ -0,0 +1,345 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- + +# pylint: disable=too-many-lines +# pylint: disable=too-many-statements + +from azure.cli.core import AzCommandsLoader +from knack.log import get_logger + + +logger = get_logger(__name__) + + + +def get_offer(cmd, + resource_group_name, + offer_name, + output_folder=None, + management_endpoint="brazilus.management.azure.com", + provider_namespace="Private.EdgeInternal", + sub_provider="Microsoft.EdgeMarketPlace", + api_version="2023-08-01-preview"): + """ + Get details of a specific marketplace offer and download its logos. + + Args: + cmd: The command context object + resource_group_name: Name of resource group + offer_name: Name of the offer to retrieve + output_folder: Folder path to save logos (optional) + management_endpoint: Management endpoint URL + provider_namespace: Provider namespace + sub_provider: Sub-provider namespace + api_version: API version + """ + import os + import json + import requests + from azure.cli.core.commands.client_factory import get_subscription_id + from azure.cli.core.util import send_raw_request + from knack.log import get_logger + + logger = get_logger(__name__) + + # Get subscription ID from current context + subscription_id = get_subscription_id(cmd.cli_ctx) + + # Construct URL with parameters + url = ( + f"https://{management_endpoint}" + f"/subscriptions/{subscription_id}" + f"/resourceGroups/{resource_group_name}" + f"/providers/{provider_namespace}/disconnectedOperations/demo-winfield1" + f"/providers/{sub_provider}/offers/{offer_name}" + f"?api-version={api_version}" + ) + + resource = "https://management.azure.com" + + try: + response = send_raw_request(cmd.cli_ctx, 'get', url, resource=resource) + + if response.status_code == 200: + data = response.json() + offer_content = data.get('properties', {}).get('offerContent', {}) + icon_uris = offer_content.get('iconFileUris', {}) + + # Download logos and metadata if output folder is specified + if output_folder: + publisher_id = offer_content.get('offerPublisher', {}).get('publisherId', '') + offer_id = offer_content.get('offerId', '') + skus = data.get('properties', {}).get('marketplaceSkus', []) + + for sku in skus: + sku_id = sku.get('marketplaceSkuId', '') + versions = sku.get('marketplaceSkuVersions', []) + + for version in versions: + version_id = version.get('name') + + # Create base path for this version + base_path = os.path.join(output_folder, 'catalog_artifacts', + publisher_id, offer_id, sku_id, version_id) + icon_path = os.path.join(base_path, 'icons') + os.makedirs(icon_path, exist_ok=True) + + # Save metadata.json + metadata_path = os.path.join(base_path, 'metadata.json') + metadata = { + 'name': data.get('name'), + 'publisher': offer_content.get('offerPublisher'), + 'offer_id': offer_content.get('offerId'), + 'summary': offer_content.get('summary'), + 'description': offer_content.get('description'), + 'sku': { + 'name': sku.get('displayName'), + 'id': sku.get('marketplaceSkuId'), + 'os_type': sku.get('operatingSystem'), + 'version': version + } + } + + with open(metadata_path, 'w', encoding='utf-8') as f: + json.dump(metadata, f, indent=2) + logger.info(f"Saved metadata to {metadata_path}") + + # Download icons + if icon_uris: + for size, uri in icon_uris.items(): + try: + logo_response = requests.get(uri) + if logo_response.status_code == 200: + file_extension = 'png' + file_path = os.path.join(icon_path, f"{size}.{file_extension}") + + with open(file_path, 'wb') as f: + f.write(logo_response.content) + logger.info(f"Downloaded {size} logo to {file_path}") + else: + logger.error(f"Failed to download {size} logo: {logo_response.status_code}") + except Exception as e: + logger.error(f"Error downloading {size} logo: {str(e)}") + + + # Format offer details + result = { + 'name': data.get('name'), + 'publisher': offer_content.get('offerPublisher', {}).get('publisherDisplayName'), + 'offer_id': offer_content.get('offerId'), + 'summary': offer_content.get('summary'), + 'description': offer_content.get('description'), + 'skus': [] + } + + # Add SKU information + skus = data.get('properties', {}).get('marketplaceSkus', []) + for sku in skus: + sku_info = { + 'name': sku.get('displayName'), + 'id': sku.get('marketplaceSkuId'), + 'os_type': sku.get('operatingSystem', {}).get('type'), + 'versions': [ + { + 'version': v.get('name'), + 'size_mb': v.get('minimumDownloadSizeInMb') + } for v in sku.get('marketplaceSkuVersions', [])[:3] # Latest 3 versions + ] + } + result['skus'].append(sku_info) + + return result + + else: + error_message = f"Request failed with status code: {response.status_code}" + logger.error(error_message) + return { + 'error': error_message, + 'status': 'failed', + 'resource_group_name': resource_group_name, + 'response': response.text + } + + except Exception as e: + logger.error(f"Failed to retrieve offer: {str(e)}") + return { + 'error': str(e), + 'status': 'failed', + 'resource_group_name': resource_group_name + } + +def get_image_download_url(cmd, + resource_group_name, + publisher, + offer, + sku, + version, + management_endpoint="brazilus.management.azure.com", + provider_namespace="Private.EdgeInternal", + api_version="2024-11-01-preview"): + """ + Get download URL for a specific marketplace image version. + """ + from azure.cli.core.commands.client_factory import get_subscription_id + from azure.cli.core.util import send_raw_request + from knack.log import get_logger + + logger = get_logger(__name__) + + # Get subscription ID + subscription_id = get_subscription_id(cmd.cli_ctx) + + # Construct URL for the listDownloadUri API + url = ( + f"https://{management_endpoint}" + f"/subscriptions/{subscription_id}" + f"/resourceGroups/{resource_group_name}" + f"/providers/{provider_namespace}/disconnectedOperations/demo-winfield1" + f"/images/{publisher}.{offer}.{sku}.{version}/listDownloadUri" + f"?api-version={api_version}" + ) + + try: + # Make POST request to get download URL + response = send_raw_request(cmd.cli_ctx, 'post', url, + resource="https://management.azure.com") + + if response.status_code == 200: + download_info = response.json() + return { + 'download_url': download_info.get('downloadUri'), + 'expiry': download_info.get('expiryTime'), + 'publisher': publisher, + 'offer': offer, + 'sku': sku, + 'version': version + } + else: + error_message = f"Failed to get download URL. Status code: {response.status_code}" + logger.error(error_message) + return { + 'error': error_message, + 'status': 'failed', + 'response': response.text + } + + except Exception as e: + logger.error(f"Error getting download URL: {str(e)}") + return { + 'error': str(e), + 'status': 'failed' + } + +def package_image(cmd, + resource_group_name, + publisher, + offer, + sku, + location): + self.kwargs.update({ + 'resource_group_name': resource_group_name, + 'publisher': publisher, + 'offer': offer, + 'sku': sku + }) + + # download metadata + + + # download the icons + + # download the image + + return { + 'resource_group_name': resource_group_name, + 'publisher': publisher, + 'offer': offer, + 'sku': sku, + 'location': location, + 'status': 'success' + } + +def list_offers(cmd, + resource_group_name, + management_endpoint="brazilus.management.azure.com", + provider_namespace="Private.EdgeInternal", + sub_provider="Microsoft.EdgeMarketPlace", + api_version="2023-08-01-preview"): + """ + List all offers for disconnected operations. + """ + from azure.cli.core.profiles import ResourceType + from azure.cli.core.commands.client_factory import get_subscription_id + from azure.cli.core.util import send_raw_request + from knack.log import get_logger + + logger = get_logger(__name__) + + # Get subscription ID from current context + subscription_id = get_subscription_id(cmd.cli_ctx) + + # Construct URL with parameters + url = ( + f"https://{management_endpoint}" + f"/subscriptions/{subscription_id}" + f"/resourceGroups/{resource_group_name}" + f"/providers/{provider_namespace}/disconnectedOperations/demo-winfield1" + f"/providers/{sub_provider}/offers" + f"?api-version={api_version}" + ) + + # Define headers with resource for authentication + headers = { + 'Content-Type': 'application/json', + } + + # Define the resource for authentication + resource = "https://management.azure.com" # Using standard Azure management endpoint + + try: + response = send_raw_request(cmd.cli_ctx, 'get', url, resource=resource) + + if response.status_code == 200: + data = response.json() + + # Format data for output + result = [] + for offer in data.get('value', []): + offer_content = offer.get('properties', {}).get('offerContent', {}) + skus = offer.get('properties', {}).get('marketplaceSkus', []) + + for sku in skus: + versions = sku.get('marketplaceSkuVersions', []) + for version in versions[:3]: # Show only latest 3 versions + row = { + 'Publisher': offer_content.get('offerPublisher', {}).get('publisherDisplayName'), + 'Offer': offer_content.get('offerId'), + 'SKU': sku.get('marketplaceSkuId'), + 'Version': version.get('name'), + 'OS_Type': sku.get('operatingSystem', {}).get('type'), + 'Size_MB': version.get('minimumDownloadSizeInMb') + } + result.append(row) + + return result + else: + error_message = f"Request failed with status code: {response.status_code}" + logger.error(error_message) + return { + 'error': error_message, + 'status': 'failed', + 'resource_group_name': resource_group_name, + 'response': response.text + } + + except Exception as e: + logger.error(f"Failed to retrieve offers: {str(e)}") + return { + 'error': str(e), + 'status': 'failed', + 'resource_group_name': resource_group_name + } \ No newline at end of file diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/tests/__init__.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/tests/__init__.py new file mode 100644 index 00000000000..5757aea3175 --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/tests/__init__.py @@ -0,0 +1,6 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/tests/latest/__init__.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/tests/latest/__init__.py new file mode 100644 index 00000000000..5757aea3175 --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/tests/latest/__init__.py @@ -0,0 +1,6 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/tests/latest/test_disconnectedoperations.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/tests/latest/test_disconnectedoperations.py new file mode 100644 index 00000000000..35610802cc7 --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/tests/latest/test_disconnectedoperations.py @@ -0,0 +1,24 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# Code generated by aaz-dev-tools +# -------------------------------------------------------------------------------------------- + +from azure.cli.testsdk import * + + +class DisconnectedoperationsScenario(ScenarioTest): + @ResourceGroupPreparer(name_prefix='cli_test_mycommand') + def test_my_command(self, resource_group): + + self.kwargs.update({ + 'resource_group_name': resource_group, + 'publisher': 'publisher', + 'offer': 'offer', + 'sku': 'sku' + }) + # Run the command and check the output + result = self.cmd('az disconnectedoperations package') + self.assertEqual(result, 'hello') + \ No newline at end of file From b3a27b79227d7ff48323f7a70aa28eade3503120 Mon Sep 17 00:00:00 2001 From: Sankalp Kotewar Date: Wed, 12 Feb 2025 09:24:58 +0530 Subject: [PATCH 11/24] fixed linter issues --- .../disconnectedoperations/_help.py | 89 ++++++++++++++++--- .../disconnectedoperations/_params.py | 54 +++++++---- .../disconnectedoperations/commands.py | 16 ++-- .../disconnectedoperations/custom.py | 80 ++++++----------- 4 files changed, 143 insertions(+), 96 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py index 492066f4645..3692f1572b1 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py @@ -24,28 +24,75 @@ - name: --management-endpoint type: string short-summary: Management endpoint URL. - default: brazilus.management.azure.com + long-summary: Uses brazilus.management.azure.com for test environment, management.azure.com for production. + default: management.azure.com - name: --provider-namespace type: string short-summary: Provider namespace. - default: Private.EdgeInternal + long-summary: Use "Private.EdgeInternal" for test environment or "Microsoft.Edge" for production. + default: Microsoft.Edge + - name: --sub-provider + type: string + short-summary: Sub-provider namespace. + default: Microsoft.EdgeMarketPlace - name: --api-version type: string short-summary: API version to use. default: 2023-08-01-preview examples: - - name: List offers in default format + - name: List offers using production environment text: az disconnectedoperations edgemarketplace listoffers -g myResourceGroup + - name: List offers using test environment + text: az disconnectedoperations edgemarketplace listoffers -g myResourceGroup --provider-namespace Private.EdgeInternal - name: List offers in table format text: az disconnectedoperations edgemarketplace listoffers -g myResourceGroup --output table - - name: List offers with custom endpoint - text: az disconnectedoperations edgemarketplace listoffers -g myResourceGroup --management-endpoint customendpoint.azure.com """ -helps['disconnectedoperations edgemarketplace packageimage'] = """ +helps['disconnectedoperations edgemarketplace get-offer'] = """ type: command - short-summary: Package a marketplace image. - long-summary: Download and package a marketplace image for use in disconnected environments. + short-summary: Get details of a specific marketplace offer. + long-summary: Retrieve detailed information about a marketplace offer and optionally download its logos. + parameters: + - name: --resource-group -g + type: string + required: true + short-summary: Name of resource group. + - name: --offer-name + type: string + required: true + short-summary: Name of the offer to retrieve. + - name: --output-folder + type: string + short-summary: Local folder path to save logos and metadata. + - name: --management-endpoint + type: string + short-summary: Management endpoint URL. + long-summary: Uses brazilus.management.azure.com for test environment, management.azure.com for production. + default: management.azure.com + - name: --provider-namespace + type: string + short-summary: Provider namespace. + long-summary: Use "Private.EdgeInternal" for test environment or "Microsoft.Edge" for production. + default: Microsoft.Edge + - name: --sub-provider + type: string + short-summary: Sub-provider namespace. + default: Microsoft.EdgeMarketPlace + - name: --api-version + type: string + short-summary: API version to use. + default: 2023-08-01-preview + examples: + - name: Get offer details using production environment + text: az disconnectedoperations edgemarketplace get-offer -g myResourceGroup --offer-name myOffer + - name: Get offer details and save logos using test environment + text: az disconnectedoperations edgemarketplace get-offer -g myResourceGroup --offer-name myOffer --output-folder ./artifacts --provider-namespace Private.EdgeInternal +""" + +helps['disconnectedoperations edgemarketplace get-image-download-url'] = """ + type: command + short-summary: Get download URL for a marketplace image. + long-summary: Get the download URL for a specific marketplace image version. parameters: - name: --resource-group -g type: string @@ -62,12 +109,28 @@ - name: --sku type: string required: true - short-summary: SKU of the marketplace image. - - name: --location -l + short-summary: SKU identifier. + - name: --version type: string required: true - short-summary: Location for the packaged image. + short-summary: Version of the marketplace image. + - name: --management-endpoint + type: string + short-summary: Management endpoint URL. + long-summary: Uses brazilus.management.azure.com for test environment, management.azure.com for production. + default: management.azure.com + - name: --provider-namespace + type: string + short-summary: Provider namespace. + long-summary: Use "Private.EdgeInternal" for test environment or "Microsoft.Edge" for production. + default: Microsoft.Edge + - name: --api-version + type: string + short-summary: API version to use. + default: 2024-11-01-preview examples: - - name: Package a Windows Server image - text: az disconnectedoperations edgemarketplace packageimage -g myResourceGroup --publisher MicrosoftWindowsServer --offer WindowsServer --sku 2019-Datacenter --location eastus + - name: Get image download URL using production environment + text: az disconnectedoperations edgemarketplace get-image-download-url -g myResourceGroup --publisher MicrosoftWindowsServer --offer WindowsServer --sku 2019-Datacenter --version latest + - name: Get image download URL using test environment + text: az disconnectedoperations edgemarketplace get-image-download-url -g myResourceGroup --publisher MicrosoftWindowsServer --offer WindowsServer --sku 2019-Datacenter --version latest --provider-namespace Private.EdgeInternal """ \ No newline at end of file diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py index ec6554388c5..23a429126ed 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py @@ -5,26 +5,44 @@ # Code generated by aaz-dev-tools # -------------------------------------------------------------------------------------------- -# pylint: disable=too-many-lines -# pylint: disable=too-many-statements - from azure.cli.core.commands.parameters import resource_group_name_type - +from knack.arguments import CLIArgumentType def load_arguments(self, _): # pylint: disable=unused-argument - with self.argument_context('disconnectedoperations edgemarketplace packageimage') as c: - c.argument('resource_group_name', arg_type=resource_group_name_type) - c.argument('publisher', options_list=['--publisher']) - c.argument('offer', options_list=['--offer']) - c.argument('sku', options_list=['--skus']) - c.argument('location', options_list=['--location']) + provider_namespace_type = CLIArgumentType( + type=str, + help='Provider namespace. Use "Private.EdgeInternal" for test environment or "Microsoft.Edge" for production', + default="Microsoft.Edge" + ) + management_endpoint_type = CLIArgumentType( + type=str, + help='Management endpoint URL. Uses brazilus.management.azure.com for test environment, management.azure.com for production', + default="management.azure.com" + ) + with self.argument_context('disconnectedoperations edgemarketplace listoffers') as c: - c.argument('management_endpoint', type=str, - help='Management endpoint URL') - c.argument('provider_namespace', type=str, - help='Provider namespace') - c.argument('sub_provider', type=str, - help='Sub-provider namespace') - c.argument('api_version', type=str, - help='API version') + c.argument('resource_group_name', arg_type=resource_group_name_type) + c.argument('management_endpoint', arg_type=management_endpoint_type) + c.argument('provider_namespace', arg_type=provider_namespace_type) + c.argument('sub_provider', type=str, help='Sub-provider namespace', default="Microsoft.EdgeMarketPlace") + c.argument('api_version', type=str, help='API version', default="2023-08-01-preview") + + with self.argument_context('disconnectedoperations edgemarketplace get-offer') as c: + c.argument('resource_group_name', arg_type=resource_group_name_type) + c.argument('offer_name', type=str, help='Name of the offer to retrieve') + c.argument('output_folder', type=str, help='Local folder path to save logos and metadata') + c.argument('management_endpoint', arg_type=management_endpoint_type) + c.argument('provider_namespace', arg_type=provider_namespace_type) + c.argument('sub_provider', type=str, help='Sub-provider namespace', default="Microsoft.EdgeMarketPlace") + c.argument('api_version', type=str, help='API version', default="2023-08-01-preview") + + with self.argument_context('disconnectedoperations edgemarketplace get-image-download-url') as c: + c.argument('resource_group_name', arg_type=resource_group_name_type) + c.argument('publisher', type=str, help='Publisher of the marketplace image') + c.argument('offer', type=str, help='Offer name') + c.argument('sku', type=str, help='SKU identifier') + c.argument('version', type=str, help='Version of the marketplace image') + c.argument('management_endpoint', arg_type=management_endpoint_type) + c.argument('provider_namespace', arg_type=provider_namespace_type) + c.argument('api_version', type=str, help='API version', default="2024-11-01-preview") \ No newline at end of file diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py index cc842ac3e1b..3aac4f08719 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py @@ -5,12 +5,6 @@ # Code generated by aaz-dev-tools # -------------------------------------------------------------------------------------------- -# pylint: disable=too-many-lines -# pylint: disable=too-many-statements - -# from azure.cli.core.commands import CliCommandType -# from azure.cli.core.profiles import ResourceType - from azure.cli.core.commands import CliCommandType def load_command_table(self, _): @@ -19,9 +13,11 @@ def load_command_table(self, _): ) with self.command_group('disconnectedoperations edgemarketplace', custom_command_type=custom_command_type) as g: - g.custom_command('packageimage', 'package_image') - g.custom_command('listoffers', 'list_offers') - g.custom_command('get-download-url', 'get_image_download_url') - g.custom_command('getoffer', 'get_offer') + g.custom_command('listoffers', 'list_offers', + help='List all marketplace offers for disconnected operations') + g.custom_command('get-image-download-url', 'get_image_download_url', + help='Get download URL for a specific marketplace image version') + g.custom_command('get-offer', 'get_offer', + help='Get details of a specific marketplace offer and download its logos') return self.command_table \ No newline at end of file diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py index 77dd8b3b112..b8c3f01c09e 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py @@ -14,29 +14,21 @@ logger = get_logger(__name__) +def _get_management_endpoint(provider_namespace): + """Helper function to determine management endpoint based on provider namespace.""" + return "brazilus.management.azure.com" if provider_namespace == "Private.EdgeInternal" else "management.azure.com" def get_offer(cmd, resource_group_name, offer_name, output_folder=None, - management_endpoint="brazilus.management.azure.com", - provider_namespace="Private.EdgeInternal", + management_endpoint=None, + provider_namespace="Microsoft.Edge", sub_provider="Microsoft.EdgeMarketPlace", api_version="2023-08-01-preview"): - """ - Get details of a specific marketplace offer and download its logos. - - Args: - cmd: The command context object - resource_group_name: Name of resource group - offer_name: Name of the offer to retrieve - output_folder: Folder path to save logos (optional) - management_endpoint: Management endpoint URL - provider_namespace: Provider namespace - sub_provider: Sub-provider namespace - api_version: API version - """ + """Get details of a specific marketplace offer and download its logos.""" + import os import json import requests @@ -44,6 +36,9 @@ def get_offer(cmd, from azure.cli.core.util import send_raw_request from knack.log import get_logger + # Use helper function if management_endpoint not explicitly provided + if management_endpoint is None: + management_endpoint = _get_management_endpoint(provider_namespace) logger = get_logger(__name__) # Get subscription ID from current context @@ -178,16 +173,18 @@ def get_image_download_url(cmd, offer, sku, version, - management_endpoint="brazilus.management.azure.com", - provider_namespace="Private.EdgeInternal", + management_endpoint=None, + provider_namespace="Microsoft.Edge", api_version="2024-11-01-preview"): - """ - Get download URL for a specific marketplace image version. - """ + """Get download URL for a specific marketplace image version.""" + from azure.cli.core.commands.client_factory import get_subscription_id from azure.cli.core.util import send_raw_request from knack.log import get_logger - + + if management_endpoint is None: + management_endpoint = _get_management_endpoint(provider_namespace) + logger = get_logger(__name__) # Get subscription ID @@ -234,44 +231,14 @@ def get_image_download_url(cmd, 'status': 'failed' } -def package_image(cmd, - resource_group_name, - publisher, - offer, - sku, - location): - self.kwargs.update({ - 'resource_group_name': resource_group_name, - 'publisher': publisher, - 'offer': offer, - 'sku': sku - }) - - # download metadata - - - # download the icons - - # download the image - - return { - 'resource_group_name': resource_group_name, - 'publisher': publisher, - 'offer': offer, - 'sku': sku, - 'location': location, - 'status': 'success' - } - def list_offers(cmd, resource_group_name, - management_endpoint="brazilus.management.azure.com", - provider_namespace="Private.EdgeInternal", + management_endpoint=None, + provider_namespace="Microsoft.Edge", sub_provider="Microsoft.EdgeMarketPlace", api_version="2023-08-01-preview"): - """ - List all offers for disconnected operations. - """ + """List all offers for disconnected operations.""" + from azure.cli.core.profiles import ResourceType from azure.cli.core.commands.client_factory import get_subscription_id from azure.cli.core.util import send_raw_request @@ -279,6 +246,9 @@ def list_offers(cmd, logger = get_logger(__name__) + if management_endpoint is None: + management_endpoint = _get_management_endpoint(provider_namespace) + # Get subscription ID from current context subscription_id = get_subscription_id(cmd.cli_ctx) From d2159a27afef5a5c0829603f45d75f2dce89bb0d Mon Sep 17 00:00:00 2001 From: Sankalp Kotewar Date: Tue, 18 Feb 2025 15:37:49 +0530 Subject: [PATCH 12/24] Added enhanced list and download logic --- .../disconnectedoperations/_params.py | 34 ++- .../disconnectedoperations/commands.py | 41 +++- .../disconnectedoperations/custom.py | 203 ++++++------------ 3 files changed, 118 insertions(+), 160 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py index 23a429126ed..166d6ed135d 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py @@ -11,38 +11,28 @@ def load_arguments(self, _): # pylint: disable=unused-argument provider_namespace_type = CLIArgumentType( type=str, - help='Provider namespace. Use "Private.EdgeInternal" for test environment or "Microsoft.Edge" for production', - default="Microsoft.Edge" + help='Provider namespace. Use "Private.EdgeInternal" for test environment or "Microsoft.EdgeMarketplace" for production', + default="Private.EdgeInternal" ) management_endpoint_type = CLIArgumentType( type=str, - help='Management endpoint URL. Uses brazilus.management.azure.com for test environment, management.azure.com for production', - default="management.azure.com" + help='Management endpoint URL. Use brazilus.management.azure.com for test environment, management.azure.com for production', + default="brazilus.management.azure.com" ) with self.argument_context('disconnectedoperations edgemarketplace listoffers') as c: c.argument('resource_group_name', arg_type=resource_group_name_type) - c.argument('management_endpoint', arg_type=management_endpoint_type) - c.argument('provider_namespace', arg_type=provider_namespace_type) - c.argument('sub_provider', type=str, help='Sub-provider namespace', default="Microsoft.EdgeMarketPlace") - c.argument('api_version', type=str, help='API version', default="2023-08-01-preview") - with self.argument_context('disconnectedoperations edgemarketplace get-offer') as c: + with self.argument_context('disconnectedoperations edgemarketplace getoffer') as c: c.argument('resource_group_name', arg_type=resource_group_name_type) c.argument('offer_name', type=str, help='Name of the offer to retrieve') - c.argument('output_folder', type=str, help='Local folder path to save logos and metadata') - c.argument('management_endpoint', arg_type=management_endpoint_type) - c.argument('provider_namespace', arg_type=provider_namespace_type) - c.argument('sub_provider', type=str, help='Sub-provider namespace', default="Microsoft.EdgeMarketPlace") - c.argument('api_version', type=str, help='API version', default="2023-08-01-preview") + c.argument('product_name', type=str, help='Name of the product to retrieve') - with self.argument_context('disconnectedoperations edgemarketplace get-image-download-url') as c: + with self.argument_context('disconnectedoperations edgemarketplace packageoffer') as c: c.argument('resource_group_name', arg_type=resource_group_name_type) - c.argument('publisher', type=str, help='Publisher of the marketplace image') - c.argument('offer', type=str, help='Offer name') - c.argument('sku', type=str, help='SKU identifier') - c.argument('version', type=str, help='Version of the marketplace image') - c.argument('management_endpoint', arg_type=management_endpoint_type) - c.argument('provider_namespace', arg_type=provider_namespace_type) - c.argument('api_version', type=str, help='API version', default="2024-11-01-preview") \ No newline at end of file + c.argument('publisher_name', type=str, help='Name of the publisher') + c.argument('offer_name', type=str, help='Name of the offer to package') + c.argument('sku', type=str, help='SKU of the product to retrieve') + c.argument('version', type=str, help='Version of the product to retrieve') + c.argument('output_folder', type=str, help='Drive and directory to save the package to. Example: E:\\ or D:\\packages\\') diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py index 3aac4f08719..57bd67d294e 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py @@ -5,7 +5,39 @@ # Code generated by aaz-dev-tools # -------------------------------------------------------------------------------------------- + from azure.cli.core.commands import CliCommandType +from collections import OrderedDict + +def transform_offers_table(result): + if not result: + return result + + # Transform each row while preserving order + transformed = [] + for item in result: + # Format versions to be on separate lines if it's a list/array + versions = item['Versions'] + if isinstance(versions, str): + # Split by comma if it's a comma-separated string + versions = [v.strip() for v in versions.split(',')] + + if isinstance(versions, (list, tuple)): + # Format each version on a new line, preserving the full format + formatted_versions = '\n'.join(str(v).strip() for v in versions) + else: + formatted_versions = str(versions) + + row = OrderedDict([ + ('Publisher', item['Publisher']), + ('Offer', item['Offer']), + ('SKU', item['SKU']), + ('Version', formatted_versions), + ('OS_Type', item['OS_Type']) + ]) + transformed.append(row) + + return transformed def load_command_table(self, _): custom_command_type = CliCommandType( @@ -13,11 +45,8 @@ def load_command_table(self, _): ) with self.command_group('disconnectedoperations edgemarketplace', custom_command_type=custom_command_type) as g: - g.custom_command('listoffers', 'list_offers', - help='List all marketplace offers for disconnected operations') - g.custom_command('get-image-download-url', 'get_image_download_url', - help='Get download URL for a specific marketplace image version') - g.custom_command('get-offer', 'get_offer', - help='Get details of a specific marketplace offer and download its logos') + g.custom_command('listoffers', 'list_offers', table_transformer=transform_offers_table) + g.custom_command('getoffer', 'get_offer') + g.custom_command('packageoffer', 'package_offer') return self.command_table \ No newline at end of file diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py index b8c3f01c09e..408fe8b0978 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py @@ -8,25 +8,22 @@ # pylint: disable=too-many-lines # pylint: disable=too-many-statements -from azure.cli.core import AzCommandsLoader from knack.log import get_logger - logger = get_logger(__name__) -def _get_management_endpoint(provider_namespace): +def _get_management_endpoint(): """Helper function to determine management endpoint based on provider namespace.""" - return "brazilus.management.azure.com" if provider_namespace == "Private.EdgeInternal" else "management.azure.com" + return "brazilus.management.azure.com" # if provider_namespace == "Private.EdgeInternal" else "management.azure.com" -def get_offer(cmd, +def package_offer(cmd, resource_group_name, + publisher_name, offer_name, - output_folder=None, - management_endpoint=None, - provider_namespace="Microsoft.Edge", - sub_provider="Microsoft.EdgeMarketPlace", - api_version="2023-08-01-preview"): + sku, + version, + output_folder): """Get details of a specific marketplace offer and download its logos.""" import os @@ -37,20 +34,23 @@ def get_offer(cmd, from knack.log import get_logger # Use helper function if management_endpoint not explicitly provided - if management_endpoint is None: - management_endpoint = _get_management_endpoint(provider_namespace) + management_endpoint = _get_management_endpoint() logger = get_logger(__name__) # Get subscription ID from current context subscription_id = get_subscription_id(cmd.cli_ctx) + provider_namespace = "Private.EdgeInternal" + sub_provider = "Microsoft.EdgeMarketPlace" + api_version = "2023-08-01-preview" + # Construct URL with parameters url = ( f"https://{management_endpoint}" f"/subscriptions/{subscription_id}" f"/resourceGroups/{resource_group_name}" f"/providers/{provider_namespace}/disconnectedOperations/demo-winfield1" - f"/providers/{sub_provider}/offers/{offer_name}" + f"/providers/{sub_provider}/offers/{publisher_name}:{offer_name}" f"?api-version={api_version}" ) @@ -73,18 +73,44 @@ def get_offer(cmd, for sku in skus: sku_id = sku.get('marketplaceSkuId', '') versions = sku.get('marketplaceSkuVersions', []) + + # If version is specified, filter for that version, else take the latest + if version: + versions = [v for v in versions if v.get('name') == version] + else: + versions = versions[:1] # Take only the latest version + + if not versions: + logger.warning(f"No matching version found for SKU {sku_id}") + continue for version in versions: version_id = version.get('name') # Create base path for this version base_path = os.path.join(output_folder, 'catalog_artifacts', - publisher_id, offer_id, sku_id, version_id) + publisher_id, offer_id, sku_id) + version_level_path = os.path.join(base_path, version_id) icon_path = os.path.join(base_path, 'icons') + + # Check if version directory exists and has content + if os.path.exists(version_level_path): + # Check if directory has any files + if os.path.exists(os.path.join(version_level_path, 'metadata.json')) or \ + any(os.scandir(version_level_path)): + error_message = f"Version directory already exists and contains files: {version_level_path}. Please delete the version folder in case you want to re-download the package." + logger.error(error_message) + return { + 'error': error_message, + 'status': 'failed', + 'path': version_level_path + } + os.makedirs(icon_path, exist_ok=True) + os.makedirs(version_level_path, exist_ok=True) # Save metadata.json - metadata_path = os.path.join(base_path, 'metadata.json') + metadata_path = os.path.join(version_level_path, 'metadata.json') metadata = { 'name': data.get('name'), 'publisher': offer_content.get('offerPublisher'), @@ -106,12 +132,17 @@ def get_offer(cmd, # Download icons if icon_uris: for size, uri in icon_uris.items(): + file_extension = 'png' + file_path = os.path.join(icon_path, f"{size}.{file_extension}") + + # Skip if icon already exists + if os.path.exists(file_path): + logger.info(f"Icon {size} already exists at {file_path}, skipping download") + continue + try: logo_response = requests.get(uri) if logo_response.status_code == 200: - file_extension = 'png' - file_path = os.path.join(icon_path, f"{size}.{file_extension}") - with open(file_path, 'wb') as f: f.write(logo_response.content) logger.info(f"Downloaded {size} logo to {file_path}") @@ -120,34 +151,7 @@ def get_offer(cmd, except Exception as e: logger.error(f"Error downloading {size} logo: {str(e)}") - - # Format offer details - result = { - 'name': data.get('name'), - 'publisher': offer_content.get('offerPublisher', {}).get('publisherDisplayName'), - 'offer_id': offer_content.get('offerId'), - 'summary': offer_content.get('summary'), - 'description': offer_content.get('description'), - 'skus': [] - } - - # Add SKU information - skus = data.get('properties', {}).get('marketplaceSkus', []) - for sku in skus: - sku_info = { - 'name': sku.get('displayName'), - 'id': sku.get('marketplaceSkuId'), - 'os_type': sku.get('operatingSystem', {}).get('type'), - 'versions': [ - { - 'version': v.get('name'), - 'size_mb': v.get('minimumDownloadSizeInMb') - } for v in sku.get('marketplaceSkuVersions', [])[:3] # Latest 3 versions - ] - } - result['skus'].append(sku_info) - - return result + print ("Metadata and icons downloaded successfully") else: error_message = f"Request failed with status code: {response.status_code}" @@ -166,91 +170,23 @@ def get_offer(cmd, 'status': 'failed', 'resource_group_name': resource_group_name } - -def get_image_download_url(cmd, - resource_group_name, - publisher, - offer, - sku, - version, - management_endpoint=None, - provider_namespace="Microsoft.Edge", - api_version="2024-11-01-preview"): - """Get download URL for a specific marketplace image version.""" - - from azure.cli.core.commands.client_factory import get_subscription_id - from azure.cli.core.util import send_raw_request - from knack.log import get_logger - - if management_endpoint is None: - management_endpoint = _get_management_endpoint(provider_namespace) - - logger = get_logger(__name__) - - # Get subscription ID - subscription_id = get_subscription_id(cmd.cli_ctx) - - # Construct URL for the listDownloadUri API - url = ( - f"https://{management_endpoint}" - f"/subscriptions/{subscription_id}" - f"/resourceGroups/{resource_group_name}" - f"/providers/{provider_namespace}/disconnectedOperations/demo-winfield1" - f"/images/{publisher}.{offer}.{sku}.{version}/listDownloadUri" - f"?api-version={api_version}" - ) - - try: - # Make POST request to get download URL - response = send_raw_request(cmd.cli_ctx, 'post', url, - resource="https://management.azure.com") - - if response.status_code == 200: - download_info = response.json() - return { - 'download_url': download_info.get('downloadUri'), - 'expiry': download_info.get('expiryTime'), - 'publisher': publisher, - 'offer': offer, - 'sku': sku, - 'version': version - } - else: - error_message = f"Failed to get download URL. Status code: {response.status_code}" - logger.error(error_message) - return { - 'error': error_message, - 'status': 'failed', - 'response': response.text - } - - except Exception as e: - logger.error(f"Error getting download URL: {str(e)}") - return { - 'error': str(e), - 'status': 'failed' - } -def list_offers(cmd, - resource_group_name, - management_endpoint=None, - provider_namespace="Microsoft.Edge", - sub_provider="Microsoft.EdgeMarketPlace", - api_version="2023-08-01-preview"): +def list_offers(cmd, resource_group_name): """List all offers for disconnected operations.""" - from azure.cli.core.profiles import ResourceType from azure.cli.core.commands.client_factory import get_subscription_id from azure.cli.core.util import send_raw_request from knack.log import get_logger logger = get_logger(__name__) - if management_endpoint is None: - management_endpoint = _get_management_endpoint(provider_namespace) + management_endpoint = _get_management_endpoint() # Get subscription ID from current context subscription_id = get_subscription_id(cmd.cli_ctx) + provider_namespace="Private.EdgeInternal" + sub_provider="Microsoft.EdgeMarketPlace" + api_version="2023-08-01-preview" # Construct URL with parameters url = ( @@ -275,27 +211,30 @@ def list_offers(cmd, if response.status_code == 200: data = response.json() - - # Format data for output result = [] + for offer in data.get('value', []): offer_content = offer.get('properties', {}).get('offerContent', {}) skus = offer.get('properties', {}).get('marketplaceSkus', []) for sku in skus: - versions = sku.get('marketplaceSkuVersions', []) - for version in versions[:3]: # Show only latest 3 versions - row = { - 'Publisher': offer_content.get('offerPublisher', {}).get('publisherDisplayName'), - 'Offer': offer_content.get('offerId'), - 'SKU': sku.get('marketplaceSkuId'), - 'Version': version.get('name'), - 'OS_Type': sku.get('operatingSystem', {}).get('type'), - 'Size_MB': version.get('minimumDownloadSizeInMb') - } - result.append(row) + versions = sku.get('marketplaceSkuVersions', [])[:] + # Format versions as comma-separated string with size + version_str = ', '.join([f"{v.get('name')}({v.get('minimumDownloadSizeInMb')}MB)" + for v in versions]) + + # Create a single row with flattened version info + row = { + 'Publisher': offer_content.get('offerPublisher', {}).get('publisherId'), + 'Offer': offer_content.get('offerId'), + 'SKU': sku.get('marketplaceSkuId'), + 'Versions': version_str, + 'OS_Type': sku.get('operatingSystem', {}).get('type') + } + result.append(row) return result + else: error_message = f"Request failed with status code: {response.status_code}" logger.error(error_message) From a6544bbf590b842701d693d4952acb890ca2bbcb Mon Sep 17 00:00:00 2001 From: Sankalp Kotewar Date: Tue, 18 Feb 2025 15:49:28 +0530 Subject: [PATCH 13/24] fixed linter comments --- .../disconnectedoperations/_help.py | 128 ++++-------------- .../disconnectedoperations/commands.py | 1 - 2 files changed, 24 insertions(+), 105 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py index 3692f1572b1..18a3361d09e 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py @@ -1,136 +1,56 @@ -from knack.help_files import helps # pylint: disable=unused-import +from knack.help_files import helps helps['disconnectedoperations'] = """ type: group - short-summary: Commands to manage disconnected operations. - long-summary: Manage Azure Edge marketplace operations in disconnected environments. + short-summary: Commands to manage Azure Disconnected Operations. + long-summary: Manage Azure Disconnected Operations for Edge marketplace offers. """ helps['disconnectedoperations edgemarketplace'] = """ type: group - short-summary: Manage Edge Marketplace operations. - long-summary: Commands to manage Edge Marketplace images and offers. + short-summary: Manage Edge marketplace offers for disconnected operations. + long-summary: Commands to list, get details, and package Edge marketplace offers for disconnected operations. """ helps['disconnectedoperations edgemarketplace listoffers'] = """ type: command - short-summary: List available marketplace offers. - long-summary: List all available marketplace offers with their SKUs and versions. - parameters: - - name: --resource-group -g - type: string - required: true - short-summary: Name of resource group. - - name: --management-endpoint - type: string - short-summary: Management endpoint URL. - long-summary: Uses brazilus.management.azure.com for test environment, management.azure.com for production. - default: management.azure.com - - name: --provider-namespace - type: string - short-summary: Provider namespace. - long-summary: Use "Private.EdgeInternal" for test environment or "Microsoft.Edge" for production. - default: Microsoft.Edge - - name: --sub-provider - type: string - short-summary: Sub-provider namespace. - default: Microsoft.EdgeMarketPlace - - name: --api-version - type: string - short-summary: API version to use. - default: 2023-08-01-preview + short-summary: List all available Edge marketplace offers. + long-summary: List all available Edge marketplace offers with their publishers, SKUs, and versions. examples: - - name: List offers using production environment + - name: List all offers in a resource group text: az disconnectedoperations edgemarketplace listoffers -g myResourceGroup - - name: List offers using test environment - text: az disconnectedoperations edgemarketplace listoffers -g myResourceGroup --provider-namespace Private.EdgeInternal - - name: List offers in table format - text: az disconnectedoperations edgemarketplace listoffers -g myResourceGroup --output table """ -helps['disconnectedoperations edgemarketplace get-offer'] = """ +helps['disconnectedoperations edgemarketplace packageoffer'] = """ type: command - short-summary: Get details of a specific marketplace offer. - long-summary: Retrieve detailed information about a marketplace offer and optionally download its logos. + short-summary: Package an Edge marketplace offer for disconnected operations. + long-summary: Download and package an Edge marketplace offer including its metadata, logos, and other artifacts. parameters: - name: --resource-group -g type: string - required: true short-summary: Name of resource group. - - name: --offer-name - type: string - required: true - short-summary: Name of the offer to retrieve. - - name: --output-folder - type: string - short-summary: Local folder path to save logos and metadata. - - name: --management-endpoint - type: string - short-summary: Management endpoint URL. - long-summary: Uses brazilus.management.azure.com for test environment, management.azure.com for production. - default: management.azure.com - - name: --provider-namespace - type: string - short-summary: Provider namespace. - long-summary: Use "Private.EdgeInternal" for test environment or "Microsoft.Edge" for production. - default: Microsoft.Edge - - name: --sub-provider - type: string - short-summary: Sub-provider namespace. - default: Microsoft.EdgeMarketPlace - - name: --api-version - type: string - short-summary: API version to use. - default: 2023-08-01-preview - examples: - - name: Get offer details using production environment - text: az disconnectedoperations edgemarketplace get-offer -g myResourceGroup --offer-name myOffer - - name: Get offer details and save logos using test environment - text: az disconnectedoperations edgemarketplace get-offer -g myResourceGroup --offer-name myOffer --output-folder ./artifacts --provider-namespace Private.EdgeInternal -""" - -helps['disconnectedoperations edgemarketplace get-image-download-url'] = """ - type: command - short-summary: Get download URL for a marketplace image. - long-summary: Get the download URL for a specific marketplace image version. - parameters: - - name: --resource-group -g - type: string required: true - short-summary: Name of resource group. - - name: --publisher + - name: --publisher-name type: string + short-summary: Name of the publisher. required: true - short-summary: Publisher of the marketplace image. - - name: --offer + - name: --offer-name type: string + short-summary: Name of the offer. required: true - short-summary: Offer name of the marketplace image. - name: --sku type: string - required: true - short-summary: SKU identifier. + short-summary: SKU of the offer. - name: --version type: string - required: true - short-summary: Version of the marketplace image. - - name: --management-endpoint - type: string - short-summary: Management endpoint URL. - long-summary: Uses brazilus.management.azure.com for test environment, management.azure.com for production. - default: management.azure.com - - name: --provider-namespace - type: string - short-summary: Provider namespace. - long-summary: Use "Private.EdgeInternal" for test environment or "Microsoft.Edge" for production. - default: Microsoft.Edge - - name: --api-version + short-summary: Version of the offer. If not specified, latest version will be used. + - name: --output-folder type: string - short-summary: API version to use. - default: 2024-11-01-preview + short-summary: Output folder path for downloaded artifacts. + required: true examples: - - name: Get image download URL using production environment - text: az disconnectedoperations edgemarketplace get-image-download-url -g myResourceGroup --publisher MicrosoftWindowsServer --offer WindowsServer --sku 2019-Datacenter --version latest - - name: Get image download URL using test environment - text: az disconnectedoperations edgemarketplace get-image-download-url -g myResourceGroup --publisher MicrosoftWindowsServer --offer WindowsServer --sku 2019-Datacenter --version latest --provider-namespace Private.EdgeInternal + - name: Package latest version of an offer + text: az disconnectedoperations edgemarketplace packageoffer -g myResourceGroup --publisher-name publisherName --offer-name offerName --output-folder ./output + - name: Package specific version of an offer + text: az disconnectedoperations edgemarketplace packageoffer -g myResourceGroup --publisher-name publisherName --offer-name offerName --version 1.0.0 --output-folder ./output """ \ No newline at end of file diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py index 57bd67d294e..609de09ab4e 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py @@ -46,7 +46,6 @@ def load_command_table(self, _): with self.command_group('disconnectedoperations edgemarketplace', custom_command_type=custom_command_type) as g: g.custom_command('listoffers', 'list_offers', table_transformer=transform_offers_table) - g.custom_command('getoffer', 'get_offer') g.custom_command('packageoffer', 'package_offer') return self.command_table \ No newline at end of file From 82077ab4adee7463bbda1515cfaf54b2e92d9185 Mon Sep 17 00:00:00 2001 From: Sankalp Kotewar Date: Wed, 19 Feb 2025 16:49:38 +0530 Subject: [PATCH 14/24] added get-offer --- .../disconnectedoperations/_help.py | 111 +++++++++++++----- .../disconnectedoperations/commands.py | 20 +++- .../disconnectedoperations/custom.py | 110 ++++++++++++++--- 3 files changed, 196 insertions(+), 45 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py index 18a3361d09e..102459b07e4 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py @@ -1,56 +1,109 @@ -from knack.help_files import helps +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- -helps['disconnectedoperations'] = """ - type: group - short-summary: Commands to manage Azure Disconnected Operations. - long-summary: Manage Azure Disconnected Operations for Edge marketplace offers. -""" +from knack.help_files import helps helps['disconnectedoperations edgemarketplace'] = """ type: group - short-summary: Manage Edge marketplace offers for disconnected operations. - long-summary: Commands to list, get details, and package Edge marketplace offers for disconnected operations. + short-summary: Manage Edge Marketplace offers for disconnected operations. + long-summary: Commands to list, get details, and package marketplace offers for disconnected operations. """ helps['disconnectedoperations edgemarketplace listoffers'] = """ type: command - short-summary: List all available Edge marketplace offers. - long-summary: List all available Edge marketplace offers with their publishers, SKUs, and versions. + short-summary: List all available marketplace offers. examples: - - name: List all offers in a resource group - text: az disconnectedoperations edgemarketplace listoffers -g myResourceGroup + - name: List all marketplace offers for a specific resource + text: > + az disconnectedoperations edgemarketplace listoffers --resource-group myResourceGroup --resource-name myResource + - name: List offers and format output as table + text: > + az disconnectedoperations edgemarketplace listoffers -g myResourceGroup -n myResource --output table + - name: List offers and filter output using JMESPath query + text: > + az disconnectedoperations edgemarketplace listoffers -g myResourceGroup -n myResource --query "[?OS_Type=='Linux']" + parameters: + - name: --resource-group -g + type: string + short-summary: Name of resource group + - name: --resource-name -n + type: string + short-summary: The resource name +""" + +helps['disconnectedoperations edgemarketplace getoffer'] = """ + type: command + short-summary: Get details of a specific marketplace offer. + examples: + - name: Get details of a specific marketplace offer + text: > + az disconnectedoperations edgemarketplace getoffer --resource-group myResourceGroup --resource-name myResource + --publisher-name publisherName --offer-name offerName + - name: Get offer details and output as JSON + text: > + az disconnectedoperations edgemarketplace getoffer -g myResourceGroup -n myResource + --publisher-name publisherName --offer-name offerName --output json + - name: Get offer details with custom query + text: > + az disconnectedoperations edgemarketplace getoffer -g myResourceGroup -n myResource + --publisher-name publisherName --offer-name offerName --query "[].{SKU:SKU,Version:Versions}" + parameters: + - name: --resource-group -g + type: string + short-summary: Name of resource group + - name: --resource-name -n + type: string + short-summary: The resource name + - name: --publisher-name + type: string + short-summary: The publisher name of the offer + - name: --offer-name + type: string + short-summary: The name of the offer """ helps['disconnectedoperations edgemarketplace packageoffer'] = """ type: command - short-summary: Package an Edge marketplace offer for disconnected operations. - long-summary: Download and package an Edge marketplace offer including its metadata, logos, and other artifacts. + short-summary: Download and package a marketplace offer with its metadata and icons. + long-summary: Downloads the marketplace offer metadata, icons, and creates a package in the specified output folder. + examples: + - name: Package a marketplace offer with specific version + text: > + az disconnectedoperations edgemarketplace packageoffer --resource-group myResourceGroup --resource-name myResource + --publisher-name publisherName --offer-name offerName --sku skuName --version versionNumber + --output-folder ./output + - name: Package latest version of an offer + text: > + az disconnectedoperations edgemarketplace packageoffer -g myResourceGroup -n myResource + --publisher-name publisherName --offer-name offerName --sku skuName + --output-folder ./latest-package + - name: Package an offer and save to a specific directory + text: > + az disconnectedoperations edgemarketplace packageoffer -g myResourceGroup -n myResource + --publisher-name publisherName --offer-name offerName --sku skuName + --output-folder "D:\\MarketplacePackages" parameters: - name: --resource-group -g type: string - short-summary: Name of resource group. - required: true + short-summary: Name of resource group + - name: --resource-name -n + type: string + short-summary: The resource name - name: --publisher-name type: string - short-summary: Name of the publisher. - required: true + short-summary: The publisher name of the offer - name: --offer-name type: string - short-summary: Name of the offer. - required: true + short-summary: The name of the offer - name: --sku type: string - short-summary: SKU of the offer. + short-summary: The SKU of the offer - name: --version type: string - short-summary: Version of the offer. If not specified, latest version will be used. + short-summary: The version of the offer (optional, latest version will be used if not specified) - name: --output-folder type: string - short-summary: Output folder path for downloaded artifacts. - required: true - examples: - - name: Package latest version of an offer - text: az disconnectedoperations edgemarketplace packageoffer -g myResourceGroup --publisher-name publisherName --offer-name offerName --output-folder ./output - - name: Package specific version of an offer - text: az disconnectedoperations edgemarketplace packageoffer -g myResourceGroup --publisher-name publisherName --offer-name offerName --version 1.0.0 --output-folder ./output + short-summary: The folder path where the package will be downloaded """ \ No newline at end of file diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py index 609de09ab4e..2bb27f47f1c 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py @@ -13,6 +13,24 @@ def transform_offers_table(result): if not result: return result + # Transform each row while preserving order + transformed = [] + for item in result: + row = OrderedDict([ + ('Publisher', item['Publisher']), + ('Offer', item['Offer']), + ('SKU', item['SKU']), + ('Version', item['Versions']), + ('OS_Type', item['OS_Type']) + ]) + transformed.append(row) + + return transformed + +def transform_offer_table(result): + if not result: + return result + # Transform each row while preserving order transformed = [] for item in result: @@ -27,7 +45,6 @@ def transform_offers_table(result): formatted_versions = '\n'.join(str(v).strip() for v in versions) else: formatted_versions = str(versions) - row = OrderedDict([ ('Publisher', item['Publisher']), ('Offer', item['Offer']), @@ -46,6 +63,7 @@ def load_command_table(self, _): with self.command_group('disconnectedoperations edgemarketplace', custom_command_type=custom_command_type) as g: g.custom_command('listoffers', 'list_offers', table_transformer=transform_offers_table) + g.custom_command('getoffer', 'get_offer', table_transformer=transform_offer_table) g.custom_command('packageoffer', 'package_offer') return self.command_table \ No newline at end of file diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py index 408fe8b0978..1f855182ef5 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py @@ -12,13 +12,15 @@ logger = get_logger(__name__) -def _get_management_endpoint(): - """Helper function to determine management endpoint based on provider namespace.""" - return "brazilus.management.azure.com" # if provider_namespace == "Private.EdgeInternal" else "management.azure.com" - +def _get_management_endpoint(cli_ctx): + """Helper function to determine management endpoint based on cloud configuration.""" + # cloud = cli_ctx.cloud + # return cloud.endpoints.resource_manager + return "brazilus.management.azure.com" # For testing purposes def package_offer(cmd, resource_group_name, + resource_name, publisher_name, offer_name, sku, @@ -34,7 +36,7 @@ def package_offer(cmd, from knack.log import get_logger # Use helper function if management_endpoint not explicitly provided - management_endpoint = _get_management_endpoint() + management_endpoint = _get_management_endpoint(cmd.cli_ctx) logger = get_logger(__name__) # Get subscription ID from current context @@ -49,7 +51,7 @@ def package_offer(cmd, f"https://{management_endpoint}" f"/subscriptions/{subscription_id}" f"/resourceGroups/{resource_group_name}" - f"/providers/{provider_namespace}/disconnectedOperations/demo-winfield1" + f"/providers/Microsoft.DataBoxEdge/dataBoxEdgeDevices/{resource_name}" f"/providers/{sub_provider}/offers/{publisher_name}:{offer_name}" f"?api-version={api_version}" ) @@ -171,7 +173,7 @@ def package_offer(cmd, 'resource_group_name': resource_group_name } -def list_offers(cmd, resource_group_name): +def list_offers(cmd, resource_group_name, resource_name): """List all offers for disconnected operations.""" from azure.cli.core.commands.client_factory import get_subscription_id @@ -180,7 +182,7 @@ def list_offers(cmd, resource_group_name): logger = get_logger(__name__) - management_endpoint = _get_management_endpoint() + management_endpoint = _get_management_endpoint(cmd.cli_ctx) # Get subscription ID from current context subscription_id = get_subscription_id(cmd.cli_ctx) @@ -193,7 +195,7 @@ def list_offers(cmd, resource_group_name): f"https://{management_endpoint}" f"/subscriptions/{subscription_id}" f"/resourceGroups/{resource_group_name}" - f"/providers/{provider_namespace}/disconnectedOperations/demo-winfield1" + f"/providers/Microsoft.DataBoxEdge/dataBoxEdgeDevices/{resource_name}" f"/providers/{sub_provider}/offers" f"?api-version={api_version}" ) @@ -219,16 +221,11 @@ def list_offers(cmd, resource_group_name): for sku in skus: versions = sku.get('marketplaceSkuVersions', [])[:] - # Format versions as comma-separated string with size - version_str = ', '.join([f"{v.get('name')}({v.get('minimumDownloadSizeInMb')}MB)" - for v in versions]) - - # Create a single row with flattened version info row = { 'Publisher': offer_content.get('offerPublisher', {}).get('publisherId'), 'Offer': offer_content.get('offerId'), 'SKU': sku.get('marketplaceSkuId'), - 'Versions': version_str, + 'Versions': f"{len(versions)} {'version' if len(versions) == 1 else 'versions'} available", 'OS_Type': sku.get('operatingSystem', {}).get('type') } result.append(row) @@ -245,6 +242,89 @@ def list_offers(cmd, resource_group_name): 'response': response.text } + except Exception as e: + logger.error(f"Failed to retrieve offers: {str(e)}") + return { + 'error': str(e), + 'status': 'failed', + 'resource_group_name': resource_group_name + } + +def get_offer(cmd, resource_group_name, resource_name, publisher_name, offer_name): + """List all offers for disconnected operations.""" + + from azure.cli.core.commands.client_factory import get_subscription_id + from azure.cli.core.util import send_raw_request + from knack.log import get_logger + + logger = get_logger(__name__) + + management_endpoint = _get_management_endpoint(cmd.cli_ctx) + + # Get subscription ID from current context + subscription_id = get_subscription_id(cmd.cli_ctx) + provider_namespace="Private.EdgeInternal" + sub_provider="Microsoft.EdgeMarketPlace" + api_version="2023-08-01-preview" + + # Construct URL with parameters + url = ( + f"https://{management_endpoint}" + f"/subscriptions/{subscription_id}" + f"/resourceGroups/{resource_group_name}" + f"/providers/Microsoft.DataBoxEdge/dataBoxEdgeDevices/{resource_name}" + f"/providers/{sub_provider}/offers/{publisher_name}:{offer_name}" + f"?api-version={api_version}" + ) + + # Define headers with resource for authentication + headers = { + 'Content-Type': 'application/json', + } + + # Define the resource for authentication + resource = "https://management.azure.com" # Using standard Azure management endpoint + + try: + response = send_raw_request(cmd.cli_ctx, 'get', url, resource=resource) + + if response.status_code == 200: + data = response.json() + result = [] + + + offer_content = data.get('properties', {}).get('offerContent', {}) + skus = data.get('properties', {}).get('marketplaceSkus', []) + + for sku in skus: + # Get all versions for this SKU + versions = sku.get('marketplaceSkuVersions', [])[:] + + # transform versions and size array into a multi-line string + version_str = ', '.join([f"{v.get('name')}({v.get('minimumDownloadSizeInMb')}MB)" + for v in versions]) + + # Create a single row with flattened version info + row = { + 'Publisher': offer_content.get('offerPublisher', {}).get('publisherId'), + 'Offer': offer_content.get('offerId'), + 'SKU': sku.get('marketplaceSkuId'), + 'Versions': version_str, + 'OS_Type': sku.get('operatingSystem', {}).get('type') + } + result.append(row) + return result + + else: + error_message = f"Request failed with status code: {response.status_code}" + logger.error(error_message) + return { + 'error': error_message, + 'status': 'failed', + 'resource_group_name': resource_group_name, + 'response': response.text + } + except Exception as e: logger.error(f"Failed to retrieve offers: {str(e)}") return { From 3a6d5090d2f8d638e8e5882d88e73ad7e9d51b91 Mon Sep 17 00:00:00 2001 From: Sankalp Kotewar Date: Wed, 19 Feb 2025 17:17:55 +0530 Subject: [PATCH 15/24] updated help file --- .../disconnectedoperations/_help.py | 29 ++++++++----------- .../disconnectedoperations/_params.py | 17 +++-------- 2 files changed, 16 insertions(+), 30 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py index 102459b07e4..910fa2075e3 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py @@ -5,6 +5,11 @@ from knack.help_files import helps +helps['disconnectedoperations'] = """ + type: group + short-summary: Manage disconnected operations. + long-summary: Commands to list, get details, and package marketplace offers for disconnected operations. +""" helps['disconnectedoperations edgemarketplace'] = """ type: group short-summary: Manage Edge Marketplace offers for disconnected operations. @@ -20,15 +25,15 @@ az disconnectedoperations edgemarketplace listoffers --resource-group myResourceGroup --resource-name myResource - name: List offers and format output as table text: > - az disconnectedoperations edgemarketplace listoffers -g myResourceGroup -n myResource --output table + az disconnectedoperations edgemarketplace listoffers -g myResourceGroup --resource-name myResource --output table - name: List offers and filter output using JMESPath query text: > - az disconnectedoperations edgemarketplace listoffers -g myResourceGroup -n myResource --query "[?OS_Type=='Linux']" + az disconnectedoperations edgemarketplace listoffers -g myResourceGroup --resource-name myResource --query "[?OS_Type=='Linux']" parameters: - name: --resource-group -g type: string short-summary: Name of resource group - - name: --resource-name -n + - name: --resource-name type: string short-summary: The resource name """ @@ -43,17 +48,17 @@ --publisher-name publisherName --offer-name offerName - name: Get offer details and output as JSON text: > - az disconnectedoperations edgemarketplace getoffer -g myResourceGroup -n myResource + az disconnectedoperations edgemarketplace getoffer -g myResourceGroup --resource-name myResource --publisher-name publisherName --offer-name offerName --output json - name: Get offer details with custom query text: > - az disconnectedoperations edgemarketplace getoffer -g myResourceGroup -n myResource + az disconnectedoperations edgemarketplace getoffer -g myResourceGroup --resource-name myResource --publisher-name publisherName --offer-name offerName --query "[].{SKU:SKU,Version:Versions}" parameters: - name: --resource-group -g type: string short-summary: Name of resource group - - name: --resource-name -n + - name: --resource-name type: string short-summary: The resource name - name: --publisher-name @@ -73,22 +78,12 @@ text: > az disconnectedoperations edgemarketplace packageoffer --resource-group myResourceGroup --resource-name myResource --publisher-name publisherName --offer-name offerName --sku skuName --version versionNumber - --output-folder ./output - - name: Package latest version of an offer - text: > - az disconnectedoperations edgemarketplace packageoffer -g myResourceGroup -n myResource - --publisher-name publisherName --offer-name offerName --sku skuName - --output-folder ./latest-package - - name: Package an offer and save to a specific directory - text: > - az disconnectedoperations edgemarketplace packageoffer -g myResourceGroup -n myResource - --publisher-name publisherName --offer-name offerName --sku skuName --output-folder "D:\\MarketplacePackages" parameters: - name: --resource-group -g type: string short-summary: Name of resource group - - name: --resource-name -n + - name: --resource-name type: string short-summary: The resource name - name: --publisher-name diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py index 166d6ed135d..b259919fe82 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py @@ -6,31 +6,22 @@ # -------------------------------------------------------------------------------------------- from azure.cli.core.commands.parameters import resource_group_name_type -from knack.arguments import CLIArgumentType -def load_arguments(self, _): # pylint: disable=unused-argument - provider_namespace_type = CLIArgumentType( - type=str, - help='Provider namespace. Use "Private.EdgeInternal" for test environment or "Microsoft.EdgeMarketplace" for production', - default="Private.EdgeInternal" - ) - - management_endpoint_type = CLIArgumentType( - type=str, - help='Management endpoint URL. Use brazilus.management.azure.com for test environment, management.azure.com for production', - default="brazilus.management.azure.com" - ) +def load_arguments(self, _): with self.argument_context('disconnectedoperations edgemarketplace listoffers') as c: c.argument('resource_group_name', arg_type=resource_group_name_type) + c.argument('resource_name', type=str, help='Name of the resource to list offers for') with self.argument_context('disconnectedoperations edgemarketplace getoffer') as c: c.argument('resource_group_name', arg_type=resource_group_name_type) + c.argument('resource_name', type=str, help='Name of the resource to list offers for') c.argument('offer_name', type=str, help='Name of the offer to retrieve') c.argument('product_name', type=str, help='Name of the product to retrieve') with self.argument_context('disconnectedoperations edgemarketplace packageoffer') as c: c.argument('resource_group_name', arg_type=resource_group_name_type) + c.argument('resource_name', type=str, help='Name of the resource to list offers for') c.argument('publisher_name', type=str, help='Name of the publisher') c.argument('offer_name', type=str, help='Name of the offer to package') c.argument('sku', type=str, help='SKU of the product to retrieve') From 5f2053d090833e4afef4ebfb5e2ee08780fc52ee Mon Sep 17 00:00:00 2001 From: Sankalp Kotewar Date: Tue, 25 Feb 2025 18:12:28 +0530 Subject: [PATCH 16/24] Added image download logic --- .../disconnectedoperations/__init__.py | 32 +- .../disconnectedoperations/_client_factory.py | 8 +- .../disconnectedoperations/_help.py | 168 +++-- .../disconnectedoperations/_params.py | 48 +- .../disconnectedoperations/commands.py | 76 ++- .../disconnectedoperations/custom.py | 633 +++++++++++++----- 6 files changed, 631 insertions(+), 334 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/__init__.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/__init__.py index 1aaa566929a..07ea9258bcb 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/__init__.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/__init__.py @@ -5,30 +5,40 @@ # Code generated by aaz-dev-tools # -------------------------------------------------------------------------------------------- -from azure.cli.core import AzCommandsLoader -from azure.cli.command_modules.disconnectedoperations._help import helps # pylint: disable=unused-import from azure.cli.command_modules.disconnectedoperations._client_factory import cf_image +from azure.cli.core import AzCommandsLoader class DisconnectedoperationsCommandsLoader(AzCommandsLoader): - def __init__(self, cli_ctx=None): from azure.cli.core.commands import CliCommandType - from azure.cli.core.profiles import ResourceType # required when using python sdk + from azure.cli.core.profiles import ( + ResourceType, # required when using python sdk + ) + disconnectedoperations_custom = CliCommandType( - operations_tmpl='azure.cli.command_modules.disconnectedoperations.custom#{}', - client_factory=cf_image) - super(DisconnectedoperationsCommandsLoader, self).__init__(cli_ctx=cli_ctx, - resource_type=ResourceType.MGMT_DISCONNECTEDOPERATIONS, # required when using python sdk - custom_command_type=disconnectedoperations_custom) + operations_tmpl="azure.cli.command_modules.disconnectedoperations.custom#{}", + client_factory=cf_image, + ) + super().__init__( + cli_ctx=cli_ctx, + resource_type=ResourceType.MGMT_DISCONNECTEDOPERATIONS, + custom_command_type=disconnectedoperations_custom, + ) def load_command_table(self, args): - from azure.cli.command_modules.disconnectedoperations.commands import load_command_table + from azure.cli.command_modules.disconnectedoperations.commands import ( + load_command_table, + ) + load_command_table(self, args) return self.command_table def load_arguments(self, command): - from azure.cli.command_modules.disconnectedoperations._params import load_arguments + from azure.cli.command_modules.disconnectedoperations._params import ( + load_arguments, + ) + load_arguments(self, command) diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_client_factory.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_client_factory.py index 20df0404d3b..ab3fbf60d18 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_client_factory.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_client_factory.py @@ -7,15 +7,9 @@ def get_disconnectedoperations_management_client(cli_ctx, *_): from azure.cli.core.commands.client_factory import get_mgmt_service_client from azure.mgmt.disconnectedoperations import DisconnectedOperationsClient + return get_mgmt_service_client(cli_ctx, DisconnectedOperationsClient) def cf_image(cli_ctx, *_): return get_disconnectedoperations_management_client(cli_ctx).image - -def cf_logos(cli_ctx, *_): - return get_disconnectedoperations_management_client(cli_ctx).logos - -def cf_metadata(cli_ctx, *_): - return get_disconnectedoperations_management_client(cli_ctx).metadata - diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py index 910fa2075e3..644668227e5 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py @@ -6,99 +6,97 @@ from knack.help_files import helps helps['disconnectedoperations'] = """ - type: group - short-summary: Manage disconnected operations. - long-summary: Commands to list, get details, and package marketplace offers for disconnected operations. +type: group +short-summary: Manage disconnected operations. """ helps['disconnectedoperations edgemarketplace'] = """ - type: group - short-summary: Manage Edge Marketplace offers for disconnected operations. - long-summary: Commands to list, get details, and package marketplace offers for disconnected operations. +type: group +short-summary: Manage Edge Marketplace offers for disconnected operations. """ helps['disconnectedoperations edgemarketplace listoffers'] = """ - type: command - short-summary: List all available marketplace offers. - examples: - - name: List all marketplace offers for a specific resource - text: > - az disconnectedoperations edgemarketplace listoffers --resource-group myResourceGroup --resource-name myResource - - name: List offers and format output as table - text: > - az disconnectedoperations edgemarketplace listoffers -g myResourceGroup --resource-name myResource --output table - - name: List offers and filter output using JMESPath query - text: > - az disconnectedoperations edgemarketplace listoffers -g myResourceGroup --resource-name myResource --query "[?OS_Type=='Linux']" - parameters: - - name: --resource-group -g - type: string - short-summary: Name of resource group - - name: --resource-name - type: string - short-summary: The resource name +type: command +short-summary: List all available marketplace offers. +examples: +- name: List all marketplace offers for a specific resource + text: > +az disconnectedoperations edgemarketplace listoffers --resource-group myResourceGroup --resource-name myResource +- name: List offers and format output as table + text: > +az disconnectedoperations edgemarketplace listoffers -g myResourceGroup --resource-name myResource --output table +- name: List offers and filter output using JMESPath query + text: > +az disconnectedoperations edgemarketplace listoffers -g myResourceGroup --resource-name myResource --query "[?OS_Type=='Linux']" +parameters: +- name: --resource-group -g + type: string + short-summary: Name of resource group +- name: --resource-name + type: string + short-summary: The resource name """ helps['disconnectedoperations edgemarketplace getoffer'] = """ - type: command - short-summary: Get details of a specific marketplace offer. - examples: - - name: Get details of a specific marketplace offer - text: > - az disconnectedoperations edgemarketplace getoffer --resource-group myResourceGroup --resource-name myResource - --publisher-name publisherName --offer-name offerName - - name: Get offer details and output as JSON - text: > - az disconnectedoperations edgemarketplace getoffer -g myResourceGroup --resource-name myResource - --publisher-name publisherName --offer-name offerName --output json - - name: Get offer details with custom query - text: > - az disconnectedoperations edgemarketplace getoffer -g myResourceGroup --resource-name myResource - --publisher-name publisherName --offer-name offerName --query "[].{SKU:SKU,Version:Versions}" - parameters: - - name: --resource-group -g - type: string - short-summary: Name of resource group - - name: --resource-name - type: string - short-summary: The resource name - - name: --publisher-name - type: string - short-summary: The publisher name of the offer - - name: --offer-name - type: string - short-summary: The name of the offer +type: command +short-summary: Get details of a specific marketplace offer. +examples: +- name: Get details of a specific marketplace offer + text: > +az disconnectedoperations edgemarketplace getoffer --resource-group myResourceGroup --resource-name myResource +--publisher-name publisherName --offer-name offerName +- name: Get offer details and output as JSON + text: > +az disconnectedoperations edgemarketplace getoffer -g myResourceGroup --resource-name myResource +--publisher-name publisherName --offer-name offerName --output json +- name: Get offer details with custom query + text: > +az disconnectedoperations edgemarketplace getoffer -g myResourceGroup --resource-name myResource +--publisher-name publisherName --offer-name offerName --query "[].{SKU:SKU,Version:Versions}" +parameters: +- name: --resource-group -g + type: string + short-summary: Name of resource group +- name: --resource-name + type: string + short-summary: The resource name +- name: --publisher-name + type: string + short-summary: The publisher name of the offer +- name: --offer-name + type: string + short-summary: The name of the offer """ helps['disconnectedoperations edgemarketplace packageoffer'] = """ - type: command - short-summary: Download and package a marketplace offer with its metadata and icons. - long-summary: Downloads the marketplace offer metadata, icons, and creates a package in the specified output folder. - examples: - - name: Package a marketplace offer with specific version - text: > - az disconnectedoperations edgemarketplace packageoffer --resource-group myResourceGroup --resource-name myResource - --publisher-name publisherName --offer-name offerName --sku skuName --version versionNumber - --output-folder "D:\\MarketplacePackages" - parameters: - - name: --resource-group -g - type: string - short-summary: Name of resource group - - name: --resource-name - type: string - short-summary: The resource name - - name: --publisher-name - type: string - short-summary: The publisher name of the offer - - name: --offer-name - type: string - short-summary: The name of the offer - - name: --sku - type: string - short-summary: The SKU of the offer - - name: --version - type: string - short-summary: The version of the offer (optional, latest version will be used if not specified) - - name: --output-folder - type: string - short-summary: The folder path where the package will be downloaded -""" \ No newline at end of file +type: command +short-summary: Download and package a marketplace offer with its metadata and icons. +long-summary: Downloads the marketplace offer metadata, icons, and creates a package in the specified output folder. +examples: +- name: Package a marketplace offer with specific version + text: > +az disconnectedoperations edgemarketplace packageoffer --resource-group myResourceGroup --resource-name myResource +--publisher-name publisherName --offer-name offerName --sku skuName --version versionNumber +--output-folder "D:\\MarketplacePackages" +parameters: +- name: --resource-group -g + type: string + short-summary: Name of resource group +- name: --resource-name + type: string + short-summary: The resource name +- name: --publisher-name + type: string + short-summary: The publisher name of the offer +- name: --offer-name + type: string + short-summary: The name of the offer +- name: --sku + type: string + short-summary: The SKU of the offer +- name: --version + type: string + short-summary: The version of the offer (optional, latest version will be used if not specified) +- name: --output-folder + type: string + short-summary: The folder path where the package will be downloaded +""" diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py index b259919fe82..d5fa03dade1 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_params.py @@ -7,23 +7,37 @@ from azure.cli.core.commands.parameters import resource_group_name_type + def load_arguments(self, _): - - with self.argument_context('disconnectedoperations edgemarketplace listoffers') as c: - c.argument('resource_group_name', arg_type=resource_group_name_type) - c.argument('resource_name', type=str, help='Name of the resource to list offers for') + with self.argument_context( + "disconnectedoperations edgemarketplace listoffers" + ) as c: + c.argument("resource_group_name", arg_type=resource_group_name_type) + c.argument( + "resource_name", type=str, help="Name of the resource to list offers for" + ) - with self.argument_context('disconnectedoperations edgemarketplace getoffer') as c: - c.argument('resource_group_name', arg_type=resource_group_name_type) - c.argument('resource_name', type=str, help='Name of the resource to list offers for') - c.argument('offer_name', type=str, help='Name of the offer to retrieve') - c.argument('product_name', type=str, help='Name of the product to retrieve') + with self.argument_context("disconnectedoperations edgemarketplace getoffer") as c: + c.argument("resource_group_name", arg_type=resource_group_name_type) + c.argument( + "resource_name", type=str, help="Name of the resource to list offers for" + ) + c.argument("offer_name", type=str, help="Name of the offer") + c.argument("publisher_name", type=str, help="Name of the publisher") - with self.argument_context('disconnectedoperations edgemarketplace packageoffer') as c: - c.argument('resource_group_name', arg_type=resource_group_name_type) - c.argument('resource_name', type=str, help='Name of the resource to list offers for') - c.argument('publisher_name', type=str, help='Name of the publisher') - c.argument('offer_name', type=str, help='Name of the offer to package') - c.argument('sku', type=str, help='SKU of the product to retrieve') - c.argument('version', type=str, help='Version of the product to retrieve') - c.argument('output_folder', type=str, help='Drive and directory to save the package to. Example: E:\\ or D:\\packages\\') + with self.argument_context( + "disconnectedoperations edgemarketplace packageoffer" + ) as c: + c.argument("resource_group_name", arg_type=resource_group_name_type) + c.argument( + "resource_name", type=str, help="Name of the resource to list offers for" + ) + c.argument("publisher_name", type=str, help="Name of the publisher") + c.argument("offer_name", type=str, help="Name of the offer to package") + c.argument("sku", type=str, help="SKU of the product") + c.argument("version", type=str, help="Version of the product") + c.argument( + "output_folder", + type=str, + help="Drive and directory to save the package to. Example: E:\\ or D:\\packages\\", + ) diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py index 2bb27f47f1c..813a9943e62 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py @@ -6,64 +6,80 @@ # -------------------------------------------------------------------------------------------- -from azure.cli.core.commands import CliCommandType from collections import OrderedDict +from azure.cli.core.commands import CliCommandType + + def transform_offers_table(result): if not result: return result - + # Transform each row while preserving order transformed = [] for item in result: - row = OrderedDict([ - ('Publisher', item['Publisher']), - ('Offer', item['Offer']), - ('SKU', item['SKU']), - ('Version', item['Versions']), - ('OS_Type', item['OS_Type']) - ]) + row = OrderedDict( + [ + ("Publisher", item["Publisher"]), + ("Offer", item["Offer"]), + ("SKU", item["SKU"]), + ("Version", item["Versions"]), + ("OS_Type", item["OS_Type"]), + ] + ) transformed.append(row) - + return transformed + def transform_offer_table(result): if not result: return result - + # Transform each row while preserving order transformed = [] for item in result: # Format versions to be on separate lines if it's a list/array - versions = item['Versions'] + versions = item["Versions"] if isinstance(versions, str): # Split by comma if it's a comma-separated string - versions = [v.strip() for v in versions.split(',')] - + versions = [v.strip() for v in versions.split(",")] + if isinstance(versions, (list, tuple)): # Format each version on a new line, preserving the full format - formatted_versions = '\n'.join(str(v).strip() for v in versions) + formatted_versions = "\n".join(str(v).strip() for v in versions) else: formatted_versions = str(versions) - row = OrderedDict([ - ('Publisher', item['Publisher']), - ('Offer', item['Offer']), - ('SKU', item['SKU']), - ('Version', formatted_versions), - ('OS_Type', item['OS_Type']) - ]) + row = OrderedDict( + [ + ("Publisher", item["Publisher"]), + ("Offer", item["Offer"]), + ("SKU", item["SKU"]), + ("Version", formatted_versions), + ("OS_Type", item["OS_Type"]), + ] + ) transformed.append(row) - + return transformed + def load_command_table(self, _): custom_command_type = CliCommandType( - operations_tmpl='azure.cli.command_modules.disconnectedoperations.custom#{}' + operations_tmpl="azure.cli.command_modules.disconnectedoperations.custom#{}" ) - with self.command_group('disconnectedoperations edgemarketplace', custom_command_type=custom_command_type) as g: - g.custom_command('listoffers', 'list_offers', table_transformer=transform_offers_table) - g.custom_command('getoffer', 'get_offer', table_transformer=transform_offer_table) - g.custom_command('packageoffer', 'package_offer') - - return self.command_table \ No newline at end of file + with self.command_group( + "disconnectedoperations edgemarketplace", + custom_command_type=custom_command_type, + is_preview=True, + ) as g: + g.custom_command( + "listoffers", "list_offers", table_transformer=transform_offers_table + ) + g.custom_command( + "getoffer", "get_offer", table_transformer=transform_offer_table + ) + g.custom_command("packageoffer", "package_offer") + + return self.command_table diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py index 1f855182ef5..e6ee98baa1f 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py @@ -8,32 +8,39 @@ # pylint: disable=too-many-lines # pylint: disable=too-many-statements -from knack.log import get_logger +provider_namespace = "Microsoft.DataBoxEdge" +sub_provider = "Microsoft.EdgeMarketPlace" +api_version = "2023-08-01-preview" -logger = get_logger(__name__) def _get_management_endpoint(cli_ctx): """Helper function to determine management endpoint based on cloud configuration.""" # cloud = cli_ctx.cloud # return cloud.endpoints.resource_manager - return "brazilus.management.azure.com" # For testing purposes - -def package_offer(cmd, - resource_group_name, - resource_name, - publisher_name, - offer_name, - sku, - version, - output_folder): + return "brazilus.management.azure.com" # For testing purposes + + +def package_offer( + cmd, + resource_group_name, + resource_name, + publisher_name, + offer_name, + sku, + version, + output_folder, +): """Get details of a specific marketplace offer and download its logos.""" - import os import json + import os + import shutil + import requests + from knack.log import get_logger + from azure.cli.core.commands.client_factory import get_subscription_id from azure.cli.core.util import send_raw_request - from knack.log import get_logger # Use helper function if management_endpoint not explicitly provided management_endpoint = _get_management_endpoint(cmd.cli_ctx) @@ -41,294 +48,552 @@ def package_offer(cmd, # Get subscription ID from current context subscription_id = get_subscription_id(cmd.cli_ctx) - - provider_namespace = "Private.EdgeInternal" - sub_provider = "Microsoft.EdgeMarketPlace" - api_version = "2023-08-01-preview" # Construct URL with parameters url = ( f"https://{management_endpoint}" f"/subscriptions/{subscription_id}" f"/resourceGroups/{resource_group_name}" - f"/providers/Microsoft.DataBoxEdge/dataBoxEdgeDevices/{resource_name}" + f"/providers/{provider_namespace}/dataBoxEdgeDevices/{resource_name}" f"/providers/{sub_provider}/offers/{publisher_name}:{offer_name}" f"?api-version={api_version}" ) resource = "https://management.azure.com" - + try: - response = send_raw_request(cmd.cli_ctx, 'get', url, resource=resource) - + response = send_raw_request(cmd.cli_ctx, "get", url, resource=resource) + if response.status_code == 200: data = response.json() - offer_content = data.get('properties', {}).get('offerContent', {}) - icon_uris = offer_content.get('iconFileUris', {}) - + offer_content = data.get("properties", {}).get("offerContent", {}) + icon_uris = offer_content.get("iconFileUris", {}) # Download logos and metadata if output folder is specified if output_folder: - publisher_id = offer_content.get('offerPublisher', {}).get('publisherId', '') - offer_id = offer_content.get('offerId', '') - skus = data.get('properties', {}).get('marketplaceSkus', []) - - for sku in skus: - sku_id = sku.get('marketplaceSkuId', '') - versions = sku.get('marketplaceSkuVersions', []) - - # If version is specified, filter for that version, else take the latest - if version: - versions = [v for v in versions if v.get('name') == version] - else: - versions = versions[:1] # Take only the latest version - - if not versions: - logger.warning(f"No matching version found for SKU {sku_id}") + publisher_id = offer_content.get("offerPublisher", {}).get( + "publisherId", "" + ) + offer_id = offer_content.get("offerId", "") + skus = data.get("properties", {}).get("marketplaceSkus", []) + + for _sku in skus: + sku_id = _sku.get("marketplaceSkuId", "") + + if sku_id != sku: continue + else: + # Store the generation information + generation = _sku.get("generation") - for version in versions: - version_id = version.get('name') - - # Create base path for this version - base_path = os.path.join(output_folder, 'catalog_artifacts', - publisher_id, offer_id, sku_id) - version_level_path = os.path.join(base_path, version_id) - icon_path = os.path.join(base_path, 'icons') - - # Check if version directory exists and has content - if os.path.exists(version_level_path): - # Check if directory has any files - if os.path.exists(os.path.join(version_level_path, 'metadata.json')) or \ - any(os.scandir(version_level_path)): - error_message = f"Version directory already exists and contains files: {version_level_path}. Please delete the version folder in case you want to re-download the package." - logger.error(error_message) - return { - 'error': error_message, - 'status': 'failed', - 'path': version_level_path - } + # Get all versions for this SKU + versions = _sku.get("marketplaceSkuVersions", []) + + versions = [v for v in versions if v.get("name") == version] + + if not versions: + logger.warning( + f"No matching version found for SKU {sku_id}" + ) + return + + # print if version and generation are found + print(f"Found VM version: {versions[0].get('name')}") + print(f"VM Generation: {generation}") - os.makedirs(icon_path, exist_ok=True) - os.makedirs(version_level_path, exist_ok=True) - - # Save metadata.json - metadata_path = os.path.join(version_level_path, 'metadata.json') - metadata = { - 'name': data.get('name'), - 'publisher': offer_content.get('offerPublisher'), - 'offer_id': offer_content.get('offerId'), - 'summary': offer_content.get('summary'), - 'description': offer_content.get('description'), - 'sku': { - 'name': sku.get('displayName'), - 'id': sku.get('marketplaceSkuId'), - 'os_type': sku.get('operatingSystem'), - 'version': version - } + version_id = versions[0].get("name") + + # check if sku is not found + if not version_id: + logger.warning(f"No matching SKU found: {sku}") + return + + # Create base path for this version + base_path = os.path.join( + output_folder, + "catalog_artifacts", + publisher_id, + offer_id, + sku_id, + ) + version_level_path = os.path.join(base_path, version_id) + icon_path = os.path.join(base_path, "icons") + + # Check if version directory exists and has content + if os.path.exists(version_level_path): + try: + # Remove directory and all its contents + shutil.rmtree(version_level_path) + logger.info( + f"Cleaned up existing version directory: {version_level_path}" + ) + except Exception as e: + error_message = f"Failed to clean up version directory {version_level_path}: {str(e)}" + logger.error(error_message) + return { + "error": error_message, + "status": "failed", + "path": version_level_path, } - - with open(metadata_path, 'w', encoding='utf-8') as f: - json.dump(metadata, f, indent=2) - logger.info(f"Saved metadata to {metadata_path}") - - # Download icons - if icon_uris: - for size, uri in icon_uris.items(): - file_extension = 'png' - file_path = os.path.join(icon_path, f"{size}.{file_extension}") - - # Skip if icon already exists - if os.path.exists(file_path): - logger.info(f"Icon {size} already exists at {file_path}, skipping download") - continue - - try: - logo_response = requests.get(uri) - if logo_response.status_code == 200: - with open(file_path, 'wb') as f: - f.write(logo_response.content) - logger.info(f"Downloaded {size} logo to {file_path}") - else: - logger.error(f"Failed to download {size} logo: {logo_response.status_code}") - except Exception as e: - logger.error(f"Error downloading {size} logo: {str(e)}") - - print ("Metadata and icons downloaded successfully") - + + os.makedirs(icon_path, exist_ok=True) + os.makedirs(version_level_path, exist_ok=True) + + # Save metadata.json + metadata_path = os.path.join(version_level_path, "metadata.json") + # Save Api response as it is on metadata.json + metadata = data + + with open(metadata_path, "w", encoding="utf-8") as f: + json.dump(metadata, f, indent=2) + logger.info(f"Saved metadata to {metadata_path}") + + # Download icons + if icon_uris: + for size, uri in icon_uris.items(): + file_extension = "png" + file_path = os.path.join(icon_path, f"{size}.{file_extension}") + + # Skip if icon already exists + if os.path.exists(file_path): + logger.info( + f"Icon {size} already exists at {file_path}, skipping download" + ) + continue + + try: + logo_response = requests.get(uri) + if logo_response.status_code == 200: + with open(file_path, "wb") as f: + f.write(logo_response.content) + logger.info(f"Downloaded {size} logo to {file_path}") + else: + logger.error( + f"Failed to download {size} logo: {logo_response.status_code}" + ) + except Exception as e: + logger.error(f"Error downloading {size} logo: {str(e)}") + + print("Metadata and icons downloaded successfully") + else: error_message = f"Request failed with status code: {response.status_code}" logger.error(error_message) return { - 'error': error_message, - 'status': 'failed', - 'resource_group_name': resource_group_name, - 'response': response.text + "error": error_message, + "status": "failed", + "resource_group_name": resource_group_name, + "response": response.text, } - + except Exception as e: logger.error(f"Failed to retrieve offer: {str(e)}") return { - 'error': str(e), - 'status': 'failed', - 'resource_group_name': resource_group_name + "error": str(e), + "status": "failed", + "resource_group_name": resource_group_name, + } + + print("Offer details retrieved successfully. Proceeding to download VHD.") + # Downloading VM image + return download_vhd( + cmd, + resource_group_name, + resource_name, + publisher_name, + offer_name, + sku, + version, + generation, + version_level_path, + ) + + +def download_vhd( + cmd, + resource_group_name, + resource_name, + publisher_name, + offer_name, + sku, + version, + generation, + output_folder, +): + """Generate access token for VHD download.""" + import json + import os + import time + from datetime import datetime + + from knack.log import get_logger + + from azure.cli.core.commands.client_factory import get_subscription_id + from azure.cli.core.util import send_raw_request + + logger = get_logger(__name__) + management_endpoint = _get_management_endpoint(cmd.cli_ctx) + subscription_id = get_subscription_id(cmd.cli_ctx) + + # API endpoint construction + url = ( + f"https://{management_endpoint}" + f"/subscriptions/{subscription_id}" + f"/resourceGroups/{resource_group_name}" + f"/providers/{provider_namespace}/dataBoxEdgeDevices/{resource_name}" + f"/providers/Microsoft.EdgeMarketPlace/offers/{publisher_name}:{offer_name}" + f"/generateAccessToken?api-version=2023-08-01-preview" + ) + + # Request body + body = { + "edgeMarketPlaceRegion": "westus", + "hypervGeneration": generation, + "marketPlaceSku": sku, + "marketPlaceSkuVersion": version, + } + + try: + print("Generating access token for VHD download...") + response = send_raw_request( + cmd.cli_ctx, + "post", + url, + resource="https://management.azure.com", + body=json.dumps(body), + ) + + print("Checking status of VHD download URL generation...") + print(response) + + # Check if the request was successful + if response.status_code not in (200, 202): + error_message = f"Request failed with status code: {response.status_code}" + logger.error(error_message) + return { + "error": error_message, + "status": "failed", + "resource_group_name": resource_group_name, + "response": response.text, + } + + # parse headers + headers = response.headers + + # get async operation URL from headers + async_operation_url = headers.get("Azure-AsyncOperation") + + # hit async operation URL until "status" in response is "Succeeded" with exponential backoff + if async_operation_url: + max_retries = 10 + base_delay = 2 # seconds + timeout = 300 # 5 minutes timeout + start_time = datetime.now() + + print("Hitting async operation URL...") + for attempt in range(max_retries): + print(f"Attempt {attempt + 1} of {max_retries}...") + try: + # Calculate exponential backoff delay + delay = base_delay * (2**attempt) + + # Check if we've exceeded timeout + if (datetime.now() - start_time).total_seconds() > timeout: + logger.error("Operation timed out after 5 minutes") + return { + "error": "Operation timed out", + "status": "failed", + "resource_group_name": resource_group_name, + } + + # Get operation status + status_response = send_raw_request( + cmd.cli_ctx, + "get", + async_operation_url, + resource="https://management.azure.com", + ) + + if status_response.status_code in (200, 202): + status_data = status_response.json() + status = status_data.get("status", "").lower() + + print("Current status:", status) + + if status == "succeeded": + logger.info("VHD download URL generation succeeded") + print(status_response) + # Get the download URL from the response + requestId = status_data.get("properties", {}).get( + "requestId" + ) + + # Obtaining SAS token using request Id + if requestId: + print( + f"Fetched request Id for VHD Download: {requestId}" + ) + + # Obtaining SAS token using request Id + token_url = ( + f"https://{management_endpoint}" + f"/subscriptions/{subscription_id}" + f"/resourceGroups/{resource_group_name}" + f"/providers/{provider_namespace}/dataBoxEdgeDevices/{resource_name}" + f"/providers/Microsoft.EdgeMarketPlace/offers/{publisher_name}:{offer_name}" + f"/getAccessToken?api-version={api_version}" + ) + + token_body = {"requestId": requestId} + + token_response = send_raw_request( + cmd.cli_ctx, + "post", + token_url, + resource="https://management.azure.com", + body=json.dumps(token_body), + ) + + if token_response.status_code == 200: + token_data = token_response.json() + + # Generate azcopy command + download_url = token_data.get("accessToken") + # diskId = token_data.get("diskId") + + # Construct the azcopy command + command = f'azcopy copy "{download_url}" "{output_folder}" --check-md5 NoCheck' + + print(command) + print("Executing command...") + + # Execute the command + os.system(command) + print("Download completed successfully.") + return { + "status": "succeeded", + "message": "Download completed successfully.", + } + else: + logger.error( + f"Failed to get access token: {token_response.status_code}" + ) + return { + "error": f"Failed to get access token: {token_response.status_code}", + "status": "failed", + } + + else: + logger.error("Download URL not found in response") + return { + "error": "Download URL not found", + "status": "failed", + } + + elif status == "failed": + error_message = status_data.get("error", {}).get( + "message", "Unknown error" + ) + logger.error(f"Operation failed: {error_message}") + return {"error": error_message, "status": "failed"} + + else: # In progress + logger.info( + f"Operation in progress... (attempt {attempt + 1}/{max_retries})" + ) + time.sleep(delay) + continue + + else: + logger.error( + f"Failed to get operation status: {status_response.status_code}" + ) + return { + "error": f"Status check failed: {status_response.status_code}", + "status": "failed", + } + + except Exception as e: + logger.error(f"Error checking operation status: {str(e)}") + time.sleep(delay) + continue + + # If we've exhausted all retries + logger.error("Maximum retry attempts reached") + return {"error": "Maximum retry attempts reached", "status": "failed"} + + except Exception as e: + logger.error(f"Failed to generate access token: {str(e)}") + return { + "error": str(e), + "status": "failed", + "resource_group_name": resource_group_name, } + def list_offers(cmd, resource_group_name, resource_name): """List all offers for disconnected operations.""" + from knack.log import get_logger + from azure.cli.core.commands.client_factory import get_subscription_id from azure.cli.core.util import send_raw_request - from knack.log import get_logger logger = get_logger(__name__) management_endpoint = _get_management_endpoint(cmd.cli_ctx) - + # Get subscription ID from current context subscription_id = get_subscription_id(cmd.cli_ctx) - provider_namespace="Private.EdgeInternal" - sub_provider="Microsoft.EdgeMarketPlace" - api_version="2023-08-01-preview" - + # Construct URL with parameters url = ( f"https://{management_endpoint}" f"/subscriptions/{subscription_id}" f"/resourceGroups/{resource_group_name}" - f"/providers/Microsoft.DataBoxEdge/dataBoxEdgeDevices/{resource_name}" + f"/providers/{provider_namespace}/dataBoxEdgeDevices/{resource_name}" f"/providers/{sub_provider}/offers" f"?api-version={api_version}" ) # Define headers with resource for authentication headers = { - 'Content-Type': 'application/json', + "Content-Type": "application/json", } # Define the resource for authentication - resource = "https://management.azure.com" # Using standard Azure management endpoint - + resource = ( + "https://management.azure.com" # Using standard Azure management endpoint + ) + try: - response = send_raw_request(cmd.cli_ctx, 'get', url, resource=resource) - + response = send_raw_request(cmd.cli_ctx, "get", url, resource=resource) + if response.status_code == 200: data = response.json() result = [] - - for offer in data.get('value', []): - offer_content = offer.get('properties', {}).get('offerContent', {}) - skus = offer.get('properties', {}).get('marketplaceSkus', []) - + + for offer in data.get("value", []): + offer_content = offer.get("properties", {}).get("offerContent", {}) + skus = offer.get("properties", {}).get("marketplaceSkus", []) + for sku in skus: - versions = sku.get('marketplaceSkuVersions', [])[:] + versions = sku.get("marketplaceSkuVersions", [])[:] row = { - 'Publisher': offer_content.get('offerPublisher', {}).get('publisherId'), - 'Offer': offer_content.get('offerId'), - 'SKU': sku.get('marketplaceSkuId'), - 'Versions': f"{len(versions)} {'version' if len(versions) == 1 else 'versions'} available", - 'OS_Type': sku.get('operatingSystem', {}).get('type') + "Publisher": offer_content.get("offerPublisher", {}).get( + "publisherId" + ), + "Offer": offer_content.get("offerId"), + "SKU": sku.get("marketplaceSkuId"), + "Versions": f"{len(versions)} {'version' if len(versions) == 1 else 'versions'} available", + "OS_Type": sku.get("operatingSystem", {}).get("type"), } result.append(row) - + return result - + else: error_message = f"Request failed with status code: {response.status_code}" logger.error(error_message) return { - 'error': error_message, - 'status': 'failed', - 'resource_group_name': resource_group_name, - 'response': response.text + "error": error_message, + "status": "failed", + "resource_group_name": resource_group_name, + "response": response.text, } - + except Exception as e: logger.error(f"Failed to retrieve offers: {str(e)}") return { - 'error': str(e), - 'status': 'failed', - 'resource_group_name': resource_group_name + "error": str(e), + "status": "failed", + "resource_group_name": resource_group_name, } - + + def get_offer(cmd, resource_group_name, resource_name, publisher_name, offer_name): """List all offers for disconnected operations.""" + from knack.log import get_logger + from azure.cli.core.commands.client_factory import get_subscription_id from azure.cli.core.util import send_raw_request - from knack.log import get_logger logger = get_logger(__name__) management_endpoint = _get_management_endpoint(cmd.cli_ctx) - + # Get subscription ID from current context subscription_id = get_subscription_id(cmd.cli_ctx) - provider_namespace="Private.EdgeInternal" - sub_provider="Microsoft.EdgeMarketPlace" - api_version="2023-08-01-preview" - + # Construct URL with parameters url = ( f"https://{management_endpoint}" f"/subscriptions/{subscription_id}" f"/resourceGroups/{resource_group_name}" - f"/providers/Microsoft.DataBoxEdge/dataBoxEdgeDevices/{resource_name}" + f"/providers/{provider_namespace}/dataBoxEdgeDevices/{resource_name}" f"/providers/{sub_provider}/offers/{publisher_name}:{offer_name}" f"?api-version={api_version}" ) # Define headers with resource for authentication headers = { - 'Content-Type': 'application/json', + "Content-Type": "application/json", } # Define the resource for authentication - resource = "https://management.azure.com" # Using standard Azure management endpoint - + resource = ( + "https://management.azure.com" # Using standard Azure management endpoint + ) + try: - response = send_raw_request(cmd.cli_ctx, 'get', url, resource=resource) - + response = send_raw_request(cmd.cli_ctx, "get", url, resource=resource) + if response.status_code == 200: data = response.json() result = [] - - offer_content = data.get('properties', {}).get('offerContent', {}) - skus = data.get('properties', {}).get('marketplaceSkus', []) + offer_content = data.get("properties", {}).get("offerContent", {}) + skus = data.get("properties", {}).get("marketplaceSkus", []) for sku in skus: # Get all versions for this SKU - versions = sku.get('marketplaceSkuVersions', [])[:] + versions = sku.get("marketplaceSkuVersions", [])[:] # transform versions and size array into a multi-line string - version_str = ', '.join([f"{v.get('name')}({v.get('minimumDownloadSizeInMb')}MB)" - for v in versions]) - + version_str = ", ".join( + [ + f"{v.get('name')}({v.get('minimumDownloadSizeInMb')}MB)" + for v in versions + ] + ) + # Create a single row with flattened version info row = { - 'Publisher': offer_content.get('offerPublisher', {}).get('publisherId'), - 'Offer': offer_content.get('offerId'), - 'SKU': sku.get('marketplaceSkuId'), - 'Versions': version_str, - 'OS_Type': sku.get('operatingSystem', {}).get('type') + "Publisher": offer_content.get("offerPublisher", {}).get( + "publisherId" + ), + "Offer": offer_content.get("offerId"), + "SKU": sku.get("marketplaceSkuId"), + "Versions": version_str, + "OS_Type": sku.get("operatingSystem", {}).get("type"), } result.append(row) return result - + else: error_message = f"Request failed with status code: {response.status_code}" logger.error(error_message) return { - 'error': error_message, - 'status': 'failed', - 'resource_group_name': resource_group_name, - 'response': response.text + "error": error_message, + "status": "failed", + "resource_group_name": resource_group_name, + "response": response.text, } - + except Exception as e: logger.error(f"Failed to retrieve offers: {str(e)}") return { - 'error': str(e), - 'status': 'failed', - 'resource_group_name': resource_group_name - } \ No newline at end of file + "error": str(e), + "status": "failed", + "resource_group_name": resource_group_name, + } From c0b969d76f3d5941b831f195aa65092b4d75543d Mon Sep 17 00:00:00 2001 From: Sankalp Kotewar Date: Tue, 25 Feb 2025 18:21:37 +0530 Subject: [PATCH 17/24] removing mgmt storage latest --- src/azure-cli-core/azure/cli/core/profiles/_shared.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/azure-cli-core/azure/cli/core/profiles/_shared.py b/src/azure-cli-core/azure/cli/core/profiles/_shared.py index 44e161f1b2d..c42d311d068 100644 --- a/src/azure-cli-core/azure/cli/core/profiles/_shared.py +++ b/src/azure-cli-core/azure/cli/core/profiles/_shared.py @@ -157,7 +157,7 @@ def default_api_version(self): AZURE_API_PROFILES = { 'latest': { ResourceType.MGMT_DISCONNECTEDOPERATIONS: '2024-12-01-preview', - ResourceType.MGMT_STORAGE: '2024-01-01', + #ResourceType.MGMT_STORAGE: '2024-01-01', ResourceType.MGMT_NETWORK: '2022-01-01', ResourceType.MGMT_COMPUTE: SDKProfile('2024-07-01', { 'resource_skus': '2019-04-01', From 65dd221e83f4d9949bc8edf8468ad53fac90e1ee Mon Sep 17 00:00:00 2001 From: Sankalp Kotewar Date: Tue, 25 Feb 2025 18:27:41 +0530 Subject: [PATCH 18/24] Added storage mgmt version back --- src/azure-cli-core/azure/cli/core/profiles/_shared.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/azure-cli-core/azure/cli/core/profiles/_shared.py b/src/azure-cli-core/azure/cli/core/profiles/_shared.py index c42d311d068..44e161f1b2d 100644 --- a/src/azure-cli-core/azure/cli/core/profiles/_shared.py +++ b/src/azure-cli-core/azure/cli/core/profiles/_shared.py @@ -157,7 +157,7 @@ def default_api_version(self): AZURE_API_PROFILES = { 'latest': { ResourceType.MGMT_DISCONNECTEDOPERATIONS: '2024-12-01-preview', - #ResourceType.MGMT_STORAGE: '2024-01-01', + ResourceType.MGMT_STORAGE: '2024-01-01', ResourceType.MGMT_NETWORK: '2022-01-01', ResourceType.MGMT_COMPUTE: SDKProfile('2024-07-01', { 'resource_skus': '2019-04-01', From d67dab0f93e63ef3ece2c7f145b20b10cdbe1dff Mon Sep 17 00:00:00 2001 From: Sankalp Kotewar Date: Mon, 3 Mar 2025 11:10:36 +0530 Subject: [PATCH 19/24] fixed styling issues --- .../disconnectedoperations/_help.py | 10 +- .../disconnectedoperations/commands.py | 4 +- .../disconnectedoperations/custom.py | 710 ++++++++---------- 3 files changed, 339 insertions(+), 385 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py index 644668227e5..3fbb58d6a8b 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py @@ -42,15 +42,15 @@ examples: - name: Get details of a specific marketplace offer text: > -az disconnectedoperations edgemarketplace getoffer --resource-group myResourceGroup --resource-name myResource +az disconnectedoperations edgemarketplace getoffer --resource-group myResourceGroup --resource-name myResource --publisher-name publisherName --offer-name offerName - name: Get offer details and output as JSON text: > -az disconnectedoperations edgemarketplace getoffer -g myResourceGroup --resource-name myResource +az disconnectedoperations edgemarketplace getoffer -g myResourceGroup --resource-name myResource --publisher-name publisherName --offer-name offerName --output json - name: Get offer details with custom query text: > -az disconnectedoperations edgemarketplace getoffer -g myResourceGroup --resource-name myResource +az disconnectedoperations edgemarketplace getoffer -g myResourceGroup --resource-name myResource --publisher-name publisherName --offer-name offerName --query "[].{SKU:SKU,Version:Versions}" parameters: - name: --resource-group -g @@ -74,8 +74,8 @@ examples: - name: Package a marketplace offer with specific version text: > -az disconnectedoperations edgemarketplace packageoffer --resource-group myResourceGroup --resource-name myResource ---publisher-name publisherName --offer-name offerName --sku skuName --version versionNumber +az disconnectedoperations edgemarketplace packageoffer --resource-group myResourceGroup --resource-name myResource +--publisher-name publisherName --offer-name offerName --sku skuName --version versionNumber --output-folder "D:\\MarketplacePackages" parameters: - name: --resource-group -g diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py index 813a9943e62..8fa7da6606c 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py @@ -23,7 +23,7 @@ def transform_offers_table(result): ("Publisher", item["Publisher"]), ("Offer", item["Offer"]), ("SKU", item["SKU"]), - ("Version", item["Versions"]), + ("Version(s)", item["Versions"]), ("OS_Type", item["OS_Type"]), ] ) @@ -55,7 +55,7 @@ def transform_offer_table(result): ("Publisher", item["Publisher"]), ("Offer", item["Offer"]), ("SKU", item["SKU"]), - ("Version", formatted_versions), + ("Version(s)", formatted_versions), ("OS_Type", item["OS_Type"]), ] ) diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py index e6ee98baa1f..a7697fe94b6 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py @@ -15,38 +15,126 @@ def _get_management_endpoint(cli_ctx): """Helper function to determine management endpoint based on cloud configuration.""" - # cloud = cli_ctx.cloud - # return cloud.endpoints.resource_manager - return "brazilus.management.azure.com" # For testing purposes - - -def package_offer( - cmd, - resource_group_name, - resource_name, - publisher_name, - offer_name, - sku, - version, - output_folder, -): - """Get details of a specific marketplace offer and download its logos.""" + cloud = cli_ctx.cloud + return cloud.endpoints.resource_manager + # return "brazilus.management.azure.com" - import json + +def _handle_directory_cleanup(version_level_path, logger): + """Helper function to clean up existing directory.""" import os import shutil + if os.path.exists(version_level_path): + try: + # Remove directory and all its contents + shutil.rmtree(version_level_path) + logger.info("Cleaned up existing version directory: %s", version_level_path) + except OSError as e: + error_message = f"Failed to clean up directory {version_level_path}: {str(e)}" + logger.error(error_message) + return { + "error": error_message, + "status": "failed", + "path": version_level_path, + } + return None + + +def _download_icons(icon_uris, icon_path, logger): + """Helper function to download icons.""" + import os + + import requests + + for size, uri in icon_uris.items(): + file_extension = "png" + file_path = os.path.join(icon_path, f"{size}.{file_extension}") + + # Skip if icon already exists + if os.path.exists(file_path): + logger.info("Icon %s already exists at %s, skipping download", size, file_path) + continue + + try: + logo_response = requests.get(uri) + if logo_response.status_code == 200: + with open(file_path, "wb") as f: + f.write(logo_response.content) + logger.info("Downloaded %s logo to %s", size, file_path) + else: + logger.error("Failed to download %s logo: %s", size, logo_response.status_code) + except requests.RequestException as e: + logger.error("Error downloading %s logo: %s", size, str(e)) + + +def _prepare_paths_and_metadata(output_folder, publisher_id, offer_id, sku, version_id, data, logger): + """Helper function to prepare directories and save metadata.""" + import json + import os + + # Create base path for this version + base_path = os.path.join(output_folder, "catalog_artifacts", publisher_id, offer_id, sku) + version_level_path = os.path.join(base_path, version_id) + icon_path = os.path.join(base_path, "icons") + + # Clean up existing directory if needed + cleanup_result = _handle_directory_cleanup(version_level_path, logger) + if cleanup_result: + return cleanup_result, None, None + + os.makedirs(icon_path, exist_ok=True) + os.makedirs(version_level_path, exist_ok=True) + + # Save metadata.json + metadata_path = os.path.join(version_level_path, "metadata.json") + with open(metadata_path, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2) + logger.info("Saved metadata to %s", metadata_path) + + return None, version_level_path, icon_path + + +def _find_sku_and_version(skus, sku, version, logger): + """Helper function to find matching SKU and version.""" + for _sku in skus: + sku_id = _sku.get("marketplaceSkuId", "") + if sku_id != sku: + continue + + # Store the generation information + generation = _sku.get("generation") + # Get all versions for this SKU + versions = _sku.get("marketplaceSkuVersions", []) + versions = [v for v in versions if v.get("name") == version] + + if not versions: + logger.warning("No matching version found for SKU %s", sku_id) + return None, None + + # print if version and generation are found + print("Found VM version: %s" % versions[0].get('name')) + print("VM Generation: %s" % generation) + version_id = versions[0].get("name") + return version_id, generation + + # If we get here, no matching SKU was found + logger.warning("No matching SKU found: %s", sku) + return None, None + + +def package_offer(cmd, resource_group_name, resource_name, publisher_name, + offer_name, sku, version, output_folder): + """Get details of a specific marketplace offer and download its logos.""" + import requests from knack.log import get_logger from azure.cli.core.commands.client_factory import get_subscription_id from azure.cli.core.util import send_raw_request - # Use helper function if management_endpoint not explicitly provided - management_endpoint = _get_management_endpoint(cmd.cli_ctx) logger = get_logger(__name__) - - # Get subscription ID from current context + management_endpoint = _get_management_endpoint(cmd.cli_ctx) subscription_id = get_subscription_id(cmd.cli_ctx) # Construct URL with parameters @@ -59,123 +147,10 @@ def package_offer( f"?api-version={api_version}" ) - resource = "https://management.azure.com" - try: - response = send_raw_request(cmd.cli_ctx, "get", url, resource=resource) - - if response.status_code == 200: - data = response.json() - offer_content = data.get("properties", {}).get("offerContent", {}) - icon_uris = offer_content.get("iconFileUris", {}) - # Download logos and metadata if output folder is specified - if output_folder: - publisher_id = offer_content.get("offerPublisher", {}).get( - "publisherId", "" - ) - offer_id = offer_content.get("offerId", "") - skus = data.get("properties", {}).get("marketplaceSkus", []) - - for _sku in skus: - sku_id = _sku.get("marketplaceSkuId", "") - - if sku_id != sku: - continue - else: - # Store the generation information - generation = _sku.get("generation") - - # Get all versions for this SKU - versions = _sku.get("marketplaceSkuVersions", []) - - versions = [v for v in versions if v.get("name") == version] - - if not versions: - logger.warning( - f"No matching version found for SKU {sku_id}" - ) - return - - # print if version and generation are found - print(f"Found VM version: {versions[0].get('name')}") - print(f"VM Generation: {generation}") - - version_id = versions[0].get("name") - - # check if sku is not found - if not version_id: - logger.warning(f"No matching SKU found: {sku}") - return - - # Create base path for this version - base_path = os.path.join( - output_folder, - "catalog_artifacts", - publisher_id, - offer_id, - sku_id, - ) - version_level_path = os.path.join(base_path, version_id) - icon_path = os.path.join(base_path, "icons") - - # Check if version directory exists and has content - if os.path.exists(version_level_path): - try: - # Remove directory and all its contents - shutil.rmtree(version_level_path) - logger.info( - f"Cleaned up existing version directory: {version_level_path}" - ) - except Exception as e: - error_message = f"Failed to clean up version directory {version_level_path}: {str(e)}" - logger.error(error_message) - return { - "error": error_message, - "status": "failed", - "path": version_level_path, - } - - os.makedirs(icon_path, exist_ok=True) - os.makedirs(version_level_path, exist_ok=True) - - # Save metadata.json - metadata_path = os.path.join(version_level_path, "metadata.json") - # Save Api response as it is on metadata.json - metadata = data - - with open(metadata_path, "w", encoding="utf-8") as f: - json.dump(metadata, f, indent=2) - logger.info(f"Saved metadata to {metadata_path}") - - # Download icons - if icon_uris: - for size, uri in icon_uris.items(): - file_extension = "png" - file_path = os.path.join(icon_path, f"{size}.{file_extension}") - - # Skip if icon already exists - if os.path.exists(file_path): - logger.info( - f"Icon {size} already exists at {file_path}, skipping download" - ) - continue - - try: - logo_response = requests.get(uri) - if logo_response.status_code == 200: - with open(file_path, "wb") as f: - f.write(logo_response.content) - logger.info(f"Downloaded {size} logo to {file_path}") - else: - logger.error( - f"Failed to download {size} logo: {logo_response.status_code}" - ) - except Exception as e: - logger.error(f"Error downloading {size} logo: {str(e)}") - - print("Metadata and icons downloaded successfully") + response = send_raw_request(cmd.cli_ctx, "get", url, resource=management_endpoint) - else: + if response.status_code != 200: error_message = f"Request failed with status code: {response.status_code}" logger.error(error_message) return { @@ -185,46 +160,191 @@ def package_offer( "response": response.text, } - except Exception as e: - logger.error(f"Failed to retrieve offer: {str(e)}") + data = response.json() + offer_content = data.get("properties", {}).get("offerContent", {}) + icon_uris = offer_content.get("iconFileUris", {}) + + # Download logos and metadata if output folder is specified + if output_folder: + publisher_id = offer_content.get("offerPublisher", {}).get("publisherId", "") + offer_id = offer_content.get("offerId", "") + skus = data.get("properties", {}).get("marketplaceSkus", []) + + # Find matching SKU and version + version_id, generation = _find_sku_and_version(skus, sku, version, logger) + + if not version_id: + return + + # Prepare directories and save metadata + result, version_level_path, icon_path = _prepare_paths_and_metadata( + output_folder, publisher_id, offer_id, sku, version_id, data, logger + ) + + if result: # Error occurred + return result + + # Download icons + if icon_uris: + _download_icons(icon_uris, icon_path, logger) + + print("Metadata and icons downloaded successfully") + print("Offer details retrieved successfully. Proceeding to download VHD.") + + # Downloading VM image + return download_vhd( + cmd, resource_group_name, resource_name, publisher_name, + offer_name, sku, version, generation, version_level_path + ) + + except requests.RequestException as e: + logger.error("Failed to retrieve offer: %s", str(e)) return { "error": str(e), "status": "failed", "resource_group_name": resource_group_name, } - print("Offer details retrieved successfully. Proceeding to download VHD.") - # Downloading VM image - return download_vhd( - cmd, - resource_group_name, - resource_name, - publisher_name, - offer_name, - sku, - version, - generation, - version_level_path, + +def _handle_token_response(token_response, output_folder, logger): + """Helper function to handle token response and download.""" + import os + + if token_response.status_code != 200: + logger.error("Failed to get access token: %s", token_response.status_code) + return { + "error": f"Failed to get access token: {token_response.status_code}", + "status": "failed", + } + + token_data = token_response.json() + download_url = token_data.get("accessToken") + + # Construct and execute azcopy command + command = f'azcopy copy "{download_url}" "{output_folder}" --check-md5 NoCheck' + print(command) + print("Executing command...") + os.system(command) + print("Download completed successfully.") + + return { + "status": "succeeded", + "message": "Download completed successfully.", + } + + +def _get_token_url(management_endpoint, subscription_id, resource_group_name, + resource_name, publisher_name, offer_name): + """Helper function to construct token URL.""" + return ( + f"https://{management_endpoint}" + f"/subscriptions/{subscription_id}" + f"/resourceGroups/{resource_group_name}" + f"/providers/{provider_namespace}/dataBoxEdgeDevices/{resource_name}" + f"/providers/Microsoft.EdgeMarketPlace/offers/{publisher_name}:{offer_name}" + f"/getAccessToken?api-version={api_version}" ) -def download_vhd( - cmd, - resource_group_name, - resource_name, - publisher_name, - offer_name, - sku, - version, - generation, - output_folder, -): - """Generate access token for VHD download.""" +def _process_async_operation(cmd, async_operation_url, logger, resource_group_name, + output_folder, subscription_id, resource_name, publisher_name, offer_name): + """Process async operation and monitor status.""" + import datetime import json - import os import time - from datetime import datetime + import requests + + from azure.cli.core.util import send_raw_request + + max_retries = 10 + base_delay = 2 # seconds + timeout = 300 # 5 minutes timeout + start_time = datetime.now() + + # Package parameters needed for token handling + management_endpoint = _get_management_endpoint(cmd.cli_ctx) + + print("Hitting async operation URL...") + for attempt in range(max_retries): + print("Attempt %s of %s..." % (attempt + 1, max_retries)) + try: + # Check timeout + if (datetime.now() - start_time).total_seconds() > timeout: + logger.error("Operation timed out after 5 minutes") + return {"error": "Operation timed out", "status": "failed"} + + # Get operation status + status_response = send_raw_request( + cmd.cli_ctx, "get", async_operation_url, + resource="https://management.azure.com" + ) + + if status_response.status_code not in (200, 202): + logger.error("Failed to get operation status: %s", status_response.status_code) + return { + "error": f"Status check failed: {status_response.status_code}", + "status": "failed", + } + + status_data = status_response.json() + status = status_data.get("status", "").lower() + print("Current status:", status) + + # Handle successful completion + if status == "succeeded": + logger.info("VHD download URL generation succeeded") + print(status_response) + requestId = status_data.get("properties", {}).get("requestId") + + if not requestId: + logger.error("Download URL not found in response") + return {"error": "Download URL not found", "status": "failed"} + + print(f"Fetched request Id for VHD Download: {requestId}") + + # Obtaining SAS token using request Id + token_url = _get_token_url( + management_endpoint, subscription_id, resource_group_name, + resource_name, publisher_name, offer_name + ) + token_body = {"requestId": requestId} + + token_response = send_raw_request( + cmd.cli_ctx, "post", token_url, + resource="https://management.azure.com", + body=json.dumps(token_body) + ) + + return _handle_token_response(token_response, output_folder, logger) + + # Handle failure + if status == "failed": + error_message = status_data.get("error", {}).get("message", "Unknown error") + logger.error("Operation failed: %s", error_message) + return {"error": error_message, "status": "failed"} + + # Still in progress, wait and retry + logger.info("Operation in progress... (attempt %s/%s)", attempt + 1, max_retries) + delay = base_delay * (2**attempt) # Exponential backoff + time.sleep(delay) + + except (requests.RequestException, ValueError) as e: + logger.error("Error checking operation status: %s", str(e)) + delay = base_delay * (2**attempt) + time.sleep(delay) + + # Exhausted all retries + logger.error("Maximum retry attempts reached") + return {"error": "Maximum retry attempts reached", "status": "failed"} + + +def download_vhd(cmd, resource_group_name, resource_name, publisher_name, + offer_name, sku, version, generation, output_folder): + """Generate access token for VHD download.""" + import json + + import requests from knack.log import get_logger from azure.cli.core.commands.client_factory import get_subscription_id @@ -255,15 +375,12 @@ def download_vhd( try: print("Generating access token for VHD download...") response = send_raw_request( - cmd.cli_ctx, - "post", - url, + cmd.cli_ctx, "post", url, resource="https://management.azure.com", - body=json.dumps(body), + body=json.dumps(body) ) print("Checking status of VHD download URL generation...") - print(response) # Check if the request was successful if response.status_code not in (200, 202): @@ -276,153 +393,25 @@ def download_vhd( "response": response.text, } - # parse headers - headers = response.headers - - # get async operation URL from headers - async_operation_url = headers.get("Azure-AsyncOperation") - - # hit async operation URL until "status" in response is "Succeeded" with exponential backoff - if async_operation_url: - max_retries = 10 - base_delay = 2 # seconds - timeout = 300 # 5 minutes timeout - start_time = datetime.now() - - print("Hitting async operation URL...") - for attempt in range(max_retries): - print(f"Attempt {attempt + 1} of {max_retries}...") - try: - # Calculate exponential backoff delay - delay = base_delay * (2**attempt) - - # Check if we've exceeded timeout - if (datetime.now() - start_time).total_seconds() > timeout: - logger.error("Operation timed out after 5 minutes") - return { - "error": "Operation timed out", - "status": "failed", - "resource_group_name": resource_group_name, - } - - # Get operation status - status_response = send_raw_request( - cmd.cli_ctx, - "get", - async_operation_url, - resource="https://management.azure.com", - ) - - if status_response.status_code in (200, 202): - status_data = status_response.json() - status = status_data.get("status", "").lower() - - print("Current status:", status) - - if status == "succeeded": - logger.info("VHD download URL generation succeeded") - print(status_response) - # Get the download URL from the response - requestId = status_data.get("properties", {}).get( - "requestId" - ) - - # Obtaining SAS token using request Id - if requestId: - print( - f"Fetched request Id for VHD Download: {requestId}" - ) - - # Obtaining SAS token using request Id - token_url = ( - f"https://{management_endpoint}" - f"/subscriptions/{subscription_id}" - f"/resourceGroups/{resource_group_name}" - f"/providers/{provider_namespace}/dataBoxEdgeDevices/{resource_name}" - f"/providers/Microsoft.EdgeMarketPlace/offers/{publisher_name}:{offer_name}" - f"/getAccessToken?api-version={api_version}" - ) - - token_body = {"requestId": requestId} - - token_response = send_raw_request( - cmd.cli_ctx, - "post", - token_url, - resource="https://management.azure.com", - body=json.dumps(token_body), - ) - - if token_response.status_code == 200: - token_data = token_response.json() - - # Generate azcopy command - download_url = token_data.get("accessToken") - # diskId = token_data.get("diskId") - - # Construct the azcopy command - command = f'azcopy copy "{download_url}" "{output_folder}" --check-md5 NoCheck' - - print(command) - print("Executing command...") - - # Execute the command - os.system(command) - print("Download completed successfully.") - return { - "status": "succeeded", - "message": "Download completed successfully.", - } - else: - logger.error( - f"Failed to get access token: {token_response.status_code}" - ) - return { - "error": f"Failed to get access token: {token_response.status_code}", - "status": "failed", - } - - else: - logger.error("Download URL not found in response") - return { - "error": "Download URL not found", - "status": "failed", - } - - elif status == "failed": - error_message = status_data.get("error", {}).get( - "message", "Unknown error" - ) - logger.error(f"Operation failed: {error_message}") - return {"error": error_message, "status": "failed"} - - else: # In progress - logger.info( - f"Operation in progress... (attempt {attempt + 1}/{max_retries})" - ) - time.sleep(delay) - continue - - else: - logger.error( - f"Failed to get operation status: {status_response.status_code}" - ) - return { - "error": f"Status check failed: {status_response.status_code}", - "status": "failed", - } - - except Exception as e: - logger.error(f"Error checking operation status: {str(e)}") - time.sleep(delay) - continue - - # If we've exhausted all retries - logger.error("Maximum retry attempts reached") - return {"error": "Maximum retry attempts reached", "status": "failed"} - - except Exception as e: - logger.error(f"Failed to generate access token: {str(e)}") + # Get async operation URL from headers + async_operation_url = response.headers.get("Azure-AsyncOperation") + + if not async_operation_url: + logger.error("Async operation URL not found in response") + return { + "error": "Async operation URL not found", + "status": "failed", + } + + # Process the async operation + return _process_async_operation( + cmd, async_operation_url, logger, resource_group_name, + output_folder, subscription_id, resource_name, + publisher_name, offer_name + ) + + except requests.RequestException as e: + logger.error("Failed to generate access token: %s", str(e)) return { "error": str(e), "status": "failed", @@ -432,17 +421,14 @@ def download_vhd( def list_offers(cmd, resource_group_name, resource_name): """List all offers for disconnected operations.""" - + import requests from knack.log import get_logger from azure.cli.core.commands.client_factory import get_subscription_id from azure.cli.core.util import send_raw_request logger = get_logger(__name__) - management_endpoint = _get_management_endpoint(cmd.cli_ctx) - - # Get subscription ID from current context subscription_id = get_subscription_id(cmd.cli_ctx) # Construct URL with parameters @@ -455,18 +441,8 @@ def list_offers(cmd, resource_group_name, resource_name): f"?api-version={api_version}" ) - # Define headers with resource for authentication - headers = { - "Content-Type": "application/json", - } - - # Define the resource for authentication - resource = ( - "https://management.azure.com" # Using standard Azure management endpoint - ) - try: - response = send_raw_request(cmd.cli_ctx, "get", url, resource=resource) + response = send_raw_request(cmd.cli_ctx, "get", url, resource="https://management.azure.com") if response.status_code == 200: data = response.json() @@ -479,9 +455,7 @@ def list_offers(cmd, resource_group_name, resource_name): for sku in skus: versions = sku.get("marketplaceSkuVersions", [])[:] row = { - "Publisher": offer_content.get("offerPublisher", {}).get( - "publisherId" - ), + "Publisher": offer_content.get("offerPublisher", {}).get("publisherId"), "Offer": offer_content.get("offerId"), "SKU": sku.get("marketplaceSkuId"), "Versions": f"{len(versions)} {'version' if len(versions) == 1 else 'versions'} available", @@ -491,18 +465,17 @@ def list_offers(cmd, resource_group_name, resource_name): return result - else: - error_message = f"Request failed with status code: {response.status_code}" - logger.error(error_message) - return { - "error": error_message, - "status": "failed", - "resource_group_name": resource_group_name, - "response": response.text, - } + error_message = f"Request failed with status code: {response.status_code}" + logger.error(error_message) + return { + "error": error_message, + "status": "failed", + "resource_group_name": resource_group_name, + "response": response.text, + } - except Exception as e: - logger.error(f"Failed to retrieve offers: {str(e)}") + except requests.RequestException as e: + logger.error("Failed to retrieve offers: %s", str(e)) return { "error": str(e), "status": "failed", @@ -512,17 +485,14 @@ def list_offers(cmd, resource_group_name, resource_name): def get_offer(cmd, resource_group_name, resource_name, publisher_name, offer_name): """List all offers for disconnected operations.""" - + import requests from knack.log import get_logger from azure.cli.core.commands.client_factory import get_subscription_id from azure.cli.core.util import send_raw_request logger = get_logger(__name__) - management_endpoint = _get_management_endpoint(cmd.cli_ctx) - - # Get subscription ID from current context subscription_id = get_subscription_id(cmd.cli_ctx) # Construct URL with parameters @@ -535,18 +505,8 @@ def get_offer(cmd, resource_group_name, resource_name, publisher_name, offer_nam f"?api-version={api_version}" ) - # Define headers with resource for authentication - headers = { - "Content-Type": "application/json", - } - - # Define the resource for authentication - resource = ( - "https://management.azure.com" # Using standard Azure management endpoint - ) - try: - response = send_raw_request(cmd.cli_ctx, "get", url, resource=resource) + response = send_raw_request(cmd.cli_ctx, "get", url, resource="https://management.azure.com") if response.status_code == 200: data = response.json() @@ -561,17 +521,12 @@ def get_offer(cmd, resource_group_name, resource_name, publisher_name, offer_nam # transform versions and size array into a multi-line string version_str = ", ".join( - [ - f"{v.get('name')}({v.get('minimumDownloadSizeInMb')}MB)" - for v in versions - ] + f"{v.get('name')}({v.get('minimumDownloadSizeInMb')}MB)" for v in versions ) # Create a single row with flattened version info row = { - "Publisher": offer_content.get("offerPublisher", {}).get( - "publisherId" - ), + "Publisher": offer_content.get("offerPublisher", {}).get("publisherId"), "Offer": offer_content.get("offerId"), "SKU": sku.get("marketplaceSkuId"), "Versions": version_str, @@ -580,18 +535,17 @@ def get_offer(cmd, resource_group_name, resource_name, publisher_name, offer_nam result.append(row) return result - else: - error_message = f"Request failed with status code: {response.status_code}" - logger.error(error_message) - return { - "error": error_message, - "status": "failed", - "resource_group_name": resource_group_name, - "response": response.text, - } + error_message = f"Request failed with status code: {response.status_code}" + logger.error(error_message) + return { + "error": error_message, + "status": "failed", + "resource_group_name": resource_group_name, + "response": response.text, + } - except Exception as e: - logger.error(f"Failed to retrieve offers: {str(e)}") + except requests.RequestException as e: + logger.error("Failed to retrieve offers: %s", str(e)) return { "error": str(e), "status": "failed", From 5f9ceff89b9acfd9d89814443145eb744c1280e2 Mon Sep 17 00:00:00 2001 From: Sankalp Kotewar Date: Mon, 3 Mar 2025 13:41:12 +0530 Subject: [PATCH 20/24] Added unit tests --- .../disconnectedoperations/custom.py | 51 +++- .../latest/test_disconnectedoperations.py | 268 +++++++++++++++++- 2 files changed, 303 insertions(+), 16 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py index a7697fe94b6..aff44b5f412 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/custom.py @@ -148,7 +148,7 @@ def package_offer(cmd, resource_group_name, resource_name, publisher_name, ) try: - response = send_raw_request(cmd.cli_ctx, "get", url, resource=management_endpoint) + response = send_raw_request(cmd.cli_ctx, "get", url, resource="https://management.azure.com") if response.status_code != 200: error_message = f"Request failed with status code: {response.status_code}" @@ -206,9 +206,27 @@ def package_offer(cmd, resource_group_name, resource_name, publisher_name, } +def _check_azcopy_available(): + """Check if azcopy is available in the system path.""" + import shutil + import subprocess + + # First try using shutil.which which is the proper way to check for executables + if shutil.which("azcopy"): + return True + + # Fallback to trying the command directly + try: + result = subprocess.run(["azcopy", "--version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False) + return result.returncode == 0 + except FileNotFoundError: + return False + + def _handle_token_response(token_response, output_folder, logger): """Helper function to handle token response and download.""" import os + import platform if token_response.status_code != 200: logger.error("Failed to get access token: %s", token_response.status_code) @@ -220,6 +238,35 @@ def _handle_token_response(token_response, output_folder, logger): token_data = token_response.json() download_url = token_data.get("accessToken") + # Check if azcopy is available + if not _check_azcopy_available(): + # Determine OS-specific download link + system = platform.system().lower() + if system == 'windows': + azcopy_url = "https://aka.ms/downloadazcopy-v10-windows" + install_instructions = "Download, extract the ZIP file, and add the extracted folder to your PATH." + elif system == 'linux': + azcopy_url = "https://aka.ms/downloadazcopy-v10-linux" + install_instructions = "Download, extract the tar.gz file, and move the azcopy binary to a directory in your PATH." + elif system == 'darwin': # macOS + azcopy_url = "https://aka.ms/downloadazcopy-v10-mac" + install_instructions = "Download, extract the .zip file, and move the azcopy binary to a directory in your PATH." + else: + azcopy_url = "https://aka.ms/downloadazcopy" + install_instructions = "Download and install AzCopy for your platform." + + error_message = ( + f"AzCopy tool not found. Please install AzCopy for your {system} system and make sure it's available in your PATH.\n" + f"Download link: {azcopy_url}\n" + f"Installation: {install_instructions}" + ) + logger.error(error_message) + return { + "error": error_message, + "status": "failed", + "download_url": azcopy_url + } + # Construct and execute azcopy command command = f'azcopy copy "{download_url}" "{output_folder}" --check-md5 NoCheck' print(command) @@ -249,9 +296,9 @@ def _get_token_url(management_endpoint, subscription_id, resource_group_name, def _process_async_operation(cmd, async_operation_url, logger, resource_group_name, output_folder, subscription_id, resource_name, publisher_name, offer_name): """Process async operation and monitor status.""" - import datetime import json import time + from datetime import datetime import requests diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/tests/latest/test_disconnectedoperations.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/tests/latest/test_disconnectedoperations.py index 35610802cc7..282f1203c55 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/tests/latest/test_disconnectedoperations.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/tests/latest/test_disconnectedoperations.py @@ -1,24 +1,264 @@ # -------------------------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. -# -# Code generated by aaz-dev-tools # -------------------------------------------------------------------------------------------- -from azure.cli.testsdk import * +import os +import unittest +from unittest import mock +import requests -class DisconnectedoperationsScenario(ScenarioTest): - @ResourceGroupPreparer(name_prefix='cli_test_mycommand') - def test_my_command(self, resource_group): +from azure.cli.command_modules.disconnectedoperations import custom +from azure.cli.testsdk import ResourceGroupPreparer, ScenarioTest + +class DisconnectedOperationsUnitTests(unittest.TestCase): + def setUp(self): + # Common mocks + self.mock_logger = mock.MagicMock() + self.mock_cmd = mock.MagicMock() + self.mock_cli_ctx = mock.MagicMock() + self.mock_cmd.cli_ctx = self.mock_cli_ctx + self.mock_cloud = mock.MagicMock() + self.mock_cli_ctx.cloud = self.mock_cloud + self.mock_cloud.endpoints.resource_manager = "management.azure.com" + + def test_get_management_endpoint(self): + """Test _get_management_endpoint returns the resource manager endpoint""" + endpoint = custom._get_management_endpoint(self.mock_cli_ctx) + self.assertEqual(endpoint, self.mock_cloud.endpoints.resource_manager) + + @mock.patch('os.path.exists') + @mock.patch('shutil.rmtree') + def test_handle_directory_cleanup_success(self, mock_rmtree, mock_exists): + """Test directory cleanup when directory exists""" + mock_exists.return_value = True + + result = custom._handle_directory_cleanup('/test/path', self.mock_logger) + + mock_exists.assert_called_once_with('/test/path') + mock_rmtree.assert_called_once_with('/test/path') + self.mock_logger.info.assert_called_once() + self.assertIsNone(result) + + @mock.patch('os.path.exists') + @mock.patch('shutil.rmtree') + def test_handle_directory_cleanup_error(self, mock_rmtree, mock_exists): + """Test directory cleanup when error occurs""" + mock_exists.return_value = True + mock_rmtree.side_effect = OSError("Test error") + + result = custom._handle_directory_cleanup('/test/path', self.mock_logger) + + mock_exists.assert_called_once_with('/test/path') + mock_rmtree.assert_called_once_with('/test/path') + self.mock_logger.error.assert_called_once() + self.assertIsNotNone(result) + self.assertEqual(result["status"], "failed") + self.assertIn("error", result) + + @mock.patch('os.path.exists') + @mock.patch('requests.get') + def test_download_icons_success(self, mock_get, mock_exists): + """Test icon download success path""" + # Setup mocks + mock_exists.return_value = False + + mock_response = mock.MagicMock() + mock_response.status_code = 200 + mock_response.content = b"fake_image_content" + mock_get.return_value = mock_response + + # Setup test data + icons = {"small": "http://example.com/small.png"} + + # Mock open to avoid actual file operations + m = mock.mock_open() + with mock.patch('builtins.open', m): + custom._download_icons(icons, '/test/icons', self.mock_logger) + + # Verify - using platform-independent path comparison + mock_get.assert_called_once_with("http://example.com/small.png") + + # Get the actual file path from the mock call + actual_call = m.call_args + actual_path = actual_call[0][0] + actual_mode = actual_call[0][1] + + # Verify the mode is correct + self.assertEqual(actual_mode, 'wb') + + # Verify path ends with the expected path (using platform-independent comparison) + expected_end = 'small.png' + print(actual_path) + self.assertTrue(actual_path.endswith(expected_end)) + + # Verify file write was called with correct content + handle = m() + handle.write.assert_called_once_with(b"fake_image_content") + self.mock_logger.info.assert_called_once() + + @mock.patch('os.path.exists') + @mock.patch('requests.get') + def test_download_icons_request_error(self, mock_get, mock_exists): + """Test icon download with request error""" + # Setup mocks + mock_exists.return_value = False + mock_get.side_effect = requests.RequestException("Connection error") # Use the imported requests module + + # Setup test data + icons = {"small": "http://example.com/small.png"} + + # Test + custom._download_icons(icons, '/test/icons', self.mock_logger) + + # Verify + mock_get.assert_called_once_with("http://example.com/small.png") + self.mock_logger.error.assert_called_once() + + @mock.patch('azure.cli.core.commands.client_factory.get_subscription_id') + @mock.patch('azure.cli.core.util.send_raw_request') + def test_list_offers_success(self, mock_send_raw_request, mock_get_subscription_id): + """Test list_offers success path""" + # Setup mocks + mock_get_subscription_id.return_value = "test-subscription" + + mock_response = mock.MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "value": [ + { + "properties": { + "offerContent": { + "offerPublisher": {"publisherId": "test-publisher"}, + "offerId": "test-offer" + }, + "marketplaceSkus": [ + { + "marketplaceSkuId": "test-sku", + "marketplaceSkuVersions": ["1.0", "2.0"], + "operatingSystem": {"type": "Windows"} + } + ] + } + } + ] + } + mock_send_raw_request.return_value = mock_response + + # Test + result = custom.list_offers(self.mock_cmd, "test-rg", "test-resource") + + # Verify + self.assertEqual(len(result), 1) + self.assertEqual(result[0]["Publisher"], "test-publisher") + self.assertEqual(result[0]["Offer"], "test-offer") + self.assertEqual(result[0]["SKU"], "test-sku") + self.assertEqual(result[0]["Versions"], "2 versions available") + + @mock.patch('azure.cli.core.commands.client_factory.get_subscription_id') + @mock.patch('azure.cli.core.util.send_raw_request') + def test_get_offer_success(self, mock_send_raw_request, mock_get_subscription_id): + """Test get_offer success path""" + # Setup mocks + mock_get_subscription_id.return_value = "test-subscription" + + mock_response = mock.MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "properties": { + "offerContent": { + "offerPublisher": {"publisherId": "test-publisher"}, + "offerId": "test-offer" + }, + "marketplaceSkus": [ + { + "marketplaceSkuId": "test-sku", + "marketplaceSkuVersions": [ + {"name": "1.0", "minimumDownloadSizeInMb": 100}, + {"name": "2.0", "minimumDownloadSizeInMb": 200} + ], + "operatingSystem": {"type": "Windows"} + } + ] + } + } + mock_send_raw_request.return_value = mock_response + + # Test + result = custom.get_offer(self.mock_cmd, "test-rg", "test-resource", "test-publisher", "test-offer") + + # Verify + self.assertEqual(len(result), 1) + self.assertEqual(result[0]["Publisher"], "test-publisher") + self.assertEqual(result[0]["Offer"], "test-offer") + self.assertEqual(result[0]["SKU"], "test-sku") + self.assertIn("1.0(100MB)", result[0]["Versions"]) + + @mock.patch('azure.cli.core.commands.client_factory.get_subscription_id') + @mock.patch('azure.cli.core.util.send_raw_request') + def test_package_offer_not_found(self, mock_send_raw_request, mock_get_subscription_id): + """Test package_offer when offer is not found""" + # Setup mocks + mock_get_subscription_id.return_value = "test-subscription" + + mock_response = mock.MagicMock() + mock_response.status_code = 404 + mock_response.text = "Not found" + mock_send_raw_request.return_value = mock_response + + # Test + result = custom.package_offer( + self.mock_cmd, "test-rg", "test-resource", + "test-publisher", "test-offer", "test-sku", "1.0", "/tmp" + ) + + # Verify + self.assertEqual(result["status"], "failed") + self.assertIn("error", result) + self.assertEqual(result["resource_group_name"], "test-rg") + + +class DisconnectedOperationsScenarioTests(ScenarioTest): + @ResourceGroupPreparer(name_prefix='cli_test_disconnectedops') + def test_list_offers(self, resource_group): + """Integration test for list_offers command""" + self.kwargs.update({ + 'resource_group': resource_group, + 'resource': self.create_random_name('edgedevice', 20) + }) + + # Skip if recording as this requires an actual Edge device + if self.is_live: + # Create Edge device first (requires additional setup) + # This would need an actual Edge device setup in the resource group + # For recording purposes, we're just showing the structure + self.cmd('az databoxedge device create -g {resource_group} -n {resource}') + + offers = self.cmd('az disconnectedoperations edgemarketplace listoffer -g {resource_group} --resource-name {resource}').get_output_in_json() + self.assertIsNotNone(offers) + # In a real test, we'd validate specific values in the output + + @ResourceGroupPreparer(name_prefix='cli_test_disconnectedops') + def test_get_offer(self, resource_group): + """Integration test for get_offer command""" self.kwargs.update({ - 'resource_group_name': resource_group, - 'publisher': 'publisher', - 'offer': 'offer', - 'sku': 'sku' + 'resource_group': resource_group, + 'resource': self.create_random_name('edgedevice', 20), + 'publisher': 'microsoftwindowsserver', + 'offer': 'windowsserver' }) - # Run the command and check the output - result = self.cmd('az disconnectedoperations package') - self.assertEqual(result, 'hello') - \ No newline at end of file + + # Skip if recording as this requires an actual Edge device + if self.is_live: + # This test would need to be updated with actual device creation + # and valid offer details that exist in your test environment + + result = self.cmd('az disconnectedoperations edgemarketplace getoffer -g {resource_group} --resource-name {resource} --publisher-name {publisher} --offer-name {offer}').get_output_in_json() + self.assertIsNotNone(result) + # Verify specific values in output for a real test + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From 977bb3f37ab4b0f6f6d445e89523973d8e6be2fb Mon Sep 17 00:00:00 2001 From: Sankalp Kotewar Date: Mon, 3 Mar 2025 14:12:02 +0530 Subject: [PATCH 21/24] added more tests and fixed linting issues --- .../disconnectedoperations/__init__.py | 4 +- .../disconnectedoperations/_help.py | 126 +++++++++--------- .../disconnectedoperations/commands.py | 9 ++ .../latest/test_disconnectedoperations.py | 40 +++++- 4 files changed, 109 insertions(+), 70 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/__init__.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/__init__.py index 07ea9258bcb..a1b6ee6947b 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/__init__.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/__init__.py @@ -12,9 +12,7 @@ class DisconnectedoperationsCommandsLoader(AzCommandsLoader): def __init__(self, cli_ctx=None): from azure.cli.core.commands import CliCommandType - from azure.cli.core.profiles import ( - ResourceType, # required when using python sdk - ) + from azure.cli.core.profiles import ResourceType disconnectedoperations_custom = CliCommandType( operations_tmpl="azure.cli.command_modules.disconnectedoperations.custom#{}", diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py index 3fbb58d6a8b..baae0a9ad4c 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py @@ -9,6 +9,7 @@ type: group short-summary: Manage disconnected operations. """ + helps['disconnectedoperations edgemarketplace'] = """ type: group short-summary: Manage Edge Marketplace offers for disconnected operations. @@ -18,53 +19,50 @@ type: command short-summary: List all available marketplace offers. examples: -- name: List all marketplace offers for a specific resource - text: > -az disconnectedoperations edgemarketplace listoffers --resource-group myResourceGroup --resource-name myResource -- name: List offers and format output as table - text: > -az disconnectedoperations edgemarketplace listoffers -g myResourceGroup --resource-name myResource --output table -- name: List offers and filter output using JMESPath query - text: > -az disconnectedoperations edgemarketplace listoffers -g myResourceGroup --resource-name myResource --query "[?OS_Type=='Linux']" + - name: List all marketplace offers for a specific resource + text: > + az disconnectedoperations edgemarketplace listoffers --resource-group myResourceGroup --resource-name myResource + - name: List offers and format output as table + text: > + az disconnectedoperations edgemarketplace listoffers -g myResourceGroup --resource-name myResource --output table + - name: List offers and filter output using JMESPath query + text: > + az disconnectedoperations edgemarketplace listoffers -g myResourceGroup --resource-name myResource --query "[?OS_Type=='Linux']" parameters: -- name: --resource-group -g - type: string - short-summary: Name of resource group -- name: --resource-name - type: string - short-summary: The resource name + - name: --resource-group -g + type: string + short-summary: Name of resource group + - name: --resource-name + type: string + short-summary: The resource name """ helps['disconnectedoperations edgemarketplace getoffer'] = """ type: command short-summary: Get details of a specific marketplace offer. examples: -- name: Get details of a specific marketplace offer - text: > -az disconnectedoperations edgemarketplace getoffer --resource-group myResourceGroup --resource-name myResource ---publisher-name publisherName --offer-name offerName -- name: Get offer details and output as JSON - text: > -az disconnectedoperations edgemarketplace getoffer -g myResourceGroup --resource-name myResource ---publisher-name publisherName --offer-name offerName --output json -- name: Get offer details with custom query - text: > -az disconnectedoperations edgemarketplace getoffer -g myResourceGroup --resource-name myResource ---publisher-name publisherName --offer-name offerName --query "[].{SKU:SKU,Version:Versions}" + - name: Get details of a specific marketplace offer + text: > + az disconnectedoperations edgemarketplace getoffer --resource-group myResourceGroup --resource-name myResource --publisher-name publisherName --offer-name offerName + - name: Get offer details and output as JSON + text: > + az disconnectedoperations edgemarketplace getoffer -g myResourceGroup --resource-name myResource --publisher-name publisherName --offer-name offerName --output json + - name: Get offer details with custom query + text: > + az disconnectedoperations edgemarketplace getoffer -g myResourceGroup --resource-name myResource --publisher-name publisherName --offer-name offerName --query "[].{SKU:SKU,Version:Versions}" parameters: -- name: --resource-group -g - type: string - short-summary: Name of resource group -- name: --resource-name - type: string - short-summary: The resource name -- name: --publisher-name - type: string - short-summary: The publisher name of the offer -- name: --offer-name - type: string - short-summary: The name of the offer + - name: --resource-group -g + type: string + short-summary: Name of resource group + - name: --resource-name + type: string + short-summary: The resource name + - name: --publisher-name + type: string + short-summary: The publisher name of the offer + - name: --offer-name + type: string + short-summary: The name of the offer """ helps['disconnectedoperations edgemarketplace packageoffer'] = """ @@ -72,31 +70,29 @@ short-summary: Download and package a marketplace offer with its metadata and icons. long-summary: Downloads the marketplace offer metadata, icons, and creates a package in the specified output folder. examples: -- name: Package a marketplace offer with specific version - text: > -az disconnectedoperations edgemarketplace packageoffer --resource-group myResourceGroup --resource-name myResource ---publisher-name publisherName --offer-name offerName --sku skuName --version versionNumber ---output-folder "D:\\MarketplacePackages" + - name: Package a marketplace offer with specific version + text: > + az disconnectedoperations edgemarketplace packageoffer --resource-group myResourceGroup --resource-name myResource --publisher-name publisherName --offer-name offerName --sku skuName --version versionNumber --output-folder "D:\\MarketplacePackages" parameters: -- name: --resource-group -g - type: string - short-summary: Name of resource group -- name: --resource-name - type: string - short-summary: The resource name -- name: --publisher-name - type: string - short-summary: The publisher name of the offer -- name: --offer-name - type: string - short-summary: The name of the offer -- name: --sku - type: string - short-summary: The SKU of the offer -- name: --version - type: string - short-summary: The version of the offer (optional, latest version will be used if not specified) -- name: --output-folder - type: string - short-summary: The folder path where the package will be downloaded + - name: --resource-group -g + type: string + short-summary: Name of resource group + - name: --resource-name + type: string + short-summary: The resource name + - name: --publisher-name + type: string + short-summary: The publisher name of the offer + - name: --offer-name + type: string + short-summary: The name of the offer + - name: --sku + type: string + short-summary: The SKU of the offer + - name: --version + type: string + short-summary: The version of the offer (optional, latest version will be used if not specified) + - name: --output-folder + type: string + short-summary: The folder path where the package will be downloaded """ diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py index 8fa7da6606c..cd43e8996f3 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py @@ -69,6 +69,15 @@ def load_command_table(self, _): operations_tmpl="azure.cli.command_modules.disconnectedoperations.custom#{}" ) + # Register the parent command group + with self.command_group( + "disconnectedoperations", + custom_command_type=custom_command_type, + is_preview=True, + ) as g: + pass # No commands directly at this level + + # Register the subgroup and its commands with self.command_group( "disconnectedoperations edgemarketplace", custom_command_type=custom_command_type, diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/tests/latest/test_disconnectedoperations.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/tests/latest/test_disconnectedoperations.py index 282f1203c55..5d633bc2b1e 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/tests/latest/test_disconnectedoperations.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/tests/latest/test_disconnectedoperations.py @@ -3,7 +3,6 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -import os import unittest from unittest import mock @@ -239,7 +238,6 @@ def test_list_offers(self, resource_group): offers = self.cmd('az disconnectedoperations edgemarketplace listoffer -g {resource_group} --resource-name {resource}').get_output_in_json() self.assertIsNotNone(offers) # In a real test, we'd validate specific values in the output - @ResourceGroupPreparer(name_prefix='cli_test_disconnectedops') def test_get_offer(self, resource_group): """Integration test for get_offer command""" @@ -258,7 +256,45 @@ def test_get_offer(self, resource_group): result = self.cmd('az disconnectedoperations edgemarketplace getoffer -g {resource_group} --resource-name {resource} --publisher-name {publisher} --offer-name {offer}').get_output_in_json() self.assertIsNotNone(result) # Verify specific values in output for a real test + + @ResourceGroupPreparer(name_prefix='cli_test_disconnectedops_params') + def test_get_offer_with_resource_group_name_parameter(self, resource_group): + """Test get_offer with explicit resource-group-name parameter""" + self.kwargs.update({ + 'resource_group': resource_group, + 'resource': self.create_random_name('edgedevice', 20), + 'publisher': 'microsoftwindowsserver', + 'offer': 'windowsserver' + }) + + # Skip if recording as this requires an actual Edge device + if self.is_live: + # Create Edge device first (requires additional setup) + self.cmd('az databoxedge device create --resource-group-name {resource_group} --name {resource}') + + # Test with the full --resource-group-name parameter + result = self.cmd('az disconnectedoperations edgemarketplace getoffer --resource-group-name {resource_group} --resource-name {resource} --publisher-name {publisher} --offer-name {offer}').get_output_in_json() + self.assertIsNotNone(result) + # In a real test with actual data, we would add more specific assertions + @ResourceGroupPreparer(name_prefix='cli_test_disconnectedops_pkg') + def test_package_offer_with_resource_group_name_parameter(self, resource_group): + """Test package_offer with explicit resource-group-name parameter""" + self.kwargs.update({ + 'resource_group': resource_group, + 'resource': self.create_random_name('edgedevice', 20), + 'publisher': 'microsoftwindowsserver', + 'offer': 'windowsserver', + 'sku': 'datacenter-core-1903-with-containers-smalldisk', + 'version': '18362.720.2003120536', + 'output_folder': self.create_temp_dir() + }) + + if self.is_live: + # Skip actual device creation in recorded tests + # Test with the full --resource-group-name parameter + result = self.cmd('az disconnectedoperations edgemarketplace packageoffer --resource-group-name {resource_group} --resource-name {resource} --publisher-name {publisher} --offer-name {offer} --sku {sku} --version {version} --output-folder {output_folder}').get_output_in_json() + self.assertIsNotNone(result) if __name__ == '__main__': unittest.main() \ No newline at end of file From 7e11fbc1606e3bc7aa507d2b261ae768d2eb81d6 Mon Sep 17 00:00:00 2001 From: Sankalp Kotewar Date: Mon, 3 Mar 2025 14:19:33 +0530 Subject: [PATCH 22/24] Updated test case --- .../tests/latest/test_disconnectedoperations.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/tests/latest/test_disconnectedoperations.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/tests/latest/test_disconnectedoperations.py index 5d633bc2b1e..f2455cb8df3 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/tests/latest/test_disconnectedoperations.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/tests/latest/test_disconnectedoperations.py @@ -233,9 +233,9 @@ def test_list_offers(self, resource_group): # Create Edge device first (requires additional setup) # This would need an actual Edge device setup in the resource group # For recording purposes, we're just showing the structure - self.cmd('az databoxedge device create -g {resource_group} -n {resource}') + self.cmd('az databoxedge device create --resource-group-name {resource_group} -n {resource}') - offers = self.cmd('az disconnectedoperations edgemarketplace listoffer -g {resource_group} --resource-name {resource}').get_output_in_json() + offers = self.cmd('az disconnectedoperations edgemarketplace listoffer --resource-group-name {resource_group} --resource-name {resource}').get_output_in_json() self.assertIsNotNone(offers) # In a real test, we'd validate specific values in the output @ResourceGroupPreparer(name_prefix='cli_test_disconnectedops') @@ -253,7 +253,7 @@ def test_get_offer(self, resource_group): # This test would need to be updated with actual device creation # and valid offer details that exist in your test environment - result = self.cmd('az disconnectedoperations edgemarketplace getoffer -g {resource_group} --resource-name {resource} --publisher-name {publisher} --offer-name {offer}').get_output_in_json() + result = self.cmd('az disconnectedoperations edgemarketplace getoffer --resource-group-name {resource_group} --resource-name {resource} --publisher-name {publisher} --offer-name {offer}').get_output_in_json() self.assertIsNotNone(result) # Verify specific values in output for a real test From caa8d91e50351de663e472eb18bf532aef42b435 Mon Sep 17 00:00:00 2001 From: Sankalp Kotewar Date: Tue, 11 Mar 2025 15:40:50 +0530 Subject: [PATCH 23/24] Addressed review comments for command and help --- .../disconnectedoperations/__init__.py | 3 +++ .../disconnectedoperations/_help.py | 27 +++++++++++-------- .../disconnectedoperations/commands.py | 8 +++--- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/__init__.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/__init__.py index a1b6ee6947b..59ae79d9124 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/__init__.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/__init__.py @@ -6,6 +6,9 @@ # -------------------------------------------------------------------------------------------- from azure.cli.command_modules.disconnectedoperations._client_factory import cf_image +from azure.cli.command_modules.disconnectedoperations._help import ( + helps, # pylint: disable=unused-import +) from azure.cli.core import AzCommandsLoader diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py index baae0a9ad4c..17f63c72364 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py @@ -10,24 +10,29 @@ short-summary: Manage disconnected operations. """ -helps['disconnectedoperations edgemarketplace'] = """ +helps['disconnectedoperations edge-marketplace'] = """ +type: group +short-summary: Manage Edge Marketplace for disconnected operations. +""" + +helps['disconnectedoperations edge-marketplace offer'] = """ type: group short-summary: Manage Edge Marketplace offers for disconnected operations. """ -helps['disconnectedoperations edgemarketplace listoffers'] = """ +helps['disconnectedoperations edge-marketplace offer list'] = """ type: command short-summary: List all available marketplace offers. examples: - name: List all marketplace offers for a specific resource text: > - az disconnectedoperations edgemarketplace listoffers --resource-group myResourceGroup --resource-name myResource + az disconnectedoperations edge-marketplace offer list --resource-group myResourceGroup --resource-name myResource - name: List offers and format output as table text: > - az disconnectedoperations edgemarketplace listoffers -g myResourceGroup --resource-name myResource --output table + az disconnectedoperations edge-marketplace offer list -g myResourceGroup --resource-name myResource --output table - name: List offers and filter output using JMESPath query text: > - az disconnectedoperations edgemarketplace listoffers -g myResourceGroup --resource-name myResource --query "[?OS_Type=='Linux']" + az disconnectedoperations edge-marketplace offer list -g myResourceGroup --resource-name myResource --query "[?OS_Type=='Linux']" parameters: - name: --resource-group -g type: string @@ -37,19 +42,19 @@ short-summary: The resource name """ -helps['disconnectedoperations edgemarketplace getoffer'] = """ +helps['disconnectedoperations edge-marketplace offer get'] = """ type: command short-summary: Get details of a specific marketplace offer. examples: - name: Get details of a specific marketplace offer text: > - az disconnectedoperations edgemarketplace getoffer --resource-group myResourceGroup --resource-name myResource --publisher-name publisherName --offer-name offerName + az disconnectedoperations edge-marketplace offer get --resource-group myResourceGroup --resource-name myResource --publisher-name publisherName --offer-name offerName - name: Get offer details and output as JSON text: > - az disconnectedoperations edgemarketplace getoffer -g myResourceGroup --resource-name myResource --publisher-name publisherName --offer-name offerName --output json + az disconnectedoperations edge-marketplace offer get -g myResourceGroup --resource-name myResource --publisher-name publisherName --offer-name offerName --output json - name: Get offer details with custom query text: > - az disconnectedoperations edgemarketplace getoffer -g myResourceGroup --resource-name myResource --publisher-name publisherName --offer-name offerName --query "[].{SKU:SKU,Version:Versions}" + az disconnectedoperations edge-marketplace offer get -g myResourceGroup --resource-name myResource --publisher-name publisherName --offer-name offerName --query "[].{SKU:SKU,Version:Versions}" parameters: - name: --resource-group -g type: string @@ -65,14 +70,14 @@ short-summary: The name of the offer """ -helps['disconnectedoperations edgemarketplace packageoffer'] = """ +helps['disconnectedoperations edge-marketplace offer package'] = """ type: command short-summary: Download and package a marketplace offer with its metadata and icons. long-summary: Downloads the marketplace offer metadata, icons, and creates a package in the specified output folder. examples: - name: Package a marketplace offer with specific version text: > - az disconnectedoperations edgemarketplace packageoffer --resource-group myResourceGroup --resource-name myResource --publisher-name publisherName --offer-name offerName --sku skuName --version versionNumber --output-folder "D:\\MarketplacePackages" + az disconnectedoperations edge-marketplace offer package --resource-group myResourceGroup --resource-name myResource --publisher-name publisherName --offer-name offerName --sku skuName --version versionNumber --output-folder "D:\\MarketplacePackages" parameters: - name: --resource-group -g type: string diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py index cd43e8996f3..f3ad496f601 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py @@ -79,16 +79,16 @@ def load_command_table(self, _): # Register the subgroup and its commands with self.command_group( - "disconnectedoperations edgemarketplace", + "disconnectedoperations edge-marketplace", custom_command_type=custom_command_type, is_preview=True, ) as g: g.custom_command( - "listoffers", "list_offers", table_transformer=transform_offers_table + "offer list", "list_offers", table_transformer=transform_offers_table ) g.custom_command( - "getoffer", "get_offer", table_transformer=transform_offer_table + "offer get", "get_offer", table_transformer=transform_offer_table ) - g.custom_command("packageoffer", "package_offer") + g.custom_command("offer package", "package_offer") return self.command_table From b73d8525be58a48d3f60dfe997159d4067283255 Mon Sep 17 00:00:00 2001 From: Sankalp Kotewar Date: Tue, 11 Mar 2025 18:40:52 +0530 Subject: [PATCH 24/24] Addressed review comments for edge --- .../disconnectedoperations/_help.py | 33 +++++++++++-------- .../disconnectedoperations/commands.py | 4 +-- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py index 17f63c72364..021292824c0 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/_help.py @@ -5,34 +5,39 @@ from knack.help_files import helps -helps['disconnectedoperations'] = """ +helps['edge'] = """ type: group -short-summary: Manage disconnected operations. +short-summary: Manage edge operations. """ -helps['disconnectedoperations edge-marketplace'] = """ +helps['edge disconnected-operation'] = """ +type: group +short-summary: Manage edge disconnected operations. +""" + +helps['edge disconnected-operation edge-marketplace'] = """ type: group short-summary: Manage Edge Marketplace for disconnected operations. """ -helps['disconnectedoperations edge-marketplace offer'] = """ +helps['edge disconnected-operation edge-marketplace offer'] = """ type: group short-summary: Manage Edge Marketplace offers for disconnected operations. """ -helps['disconnectedoperations edge-marketplace offer list'] = """ +helps['edge disconnected-operation edge-marketplace offer list'] = """ type: command short-summary: List all available marketplace offers. examples: - name: List all marketplace offers for a specific resource text: > - az disconnectedoperations edge-marketplace offer list --resource-group myResourceGroup --resource-name myResource + az edge disconnected-operation edge-marketplace offer list --resource-group myResourceGroup --resource-name myResource - name: List offers and format output as table text: > - az disconnectedoperations edge-marketplace offer list -g myResourceGroup --resource-name myResource --output table + az edge disconnected-operation edge-marketplace offer list -g myResourceGroup --resource-name myResource --output table - name: List offers and filter output using JMESPath query text: > - az disconnectedoperations edge-marketplace offer list -g myResourceGroup --resource-name myResource --query "[?OS_Type=='Linux']" + az edge disconnected-operation edge-marketplace offer list -g myResourceGroup --resource-name myResource --query "[?OS_Type=='Linux']" parameters: - name: --resource-group -g type: string @@ -42,19 +47,19 @@ short-summary: The resource name """ -helps['disconnectedoperations edge-marketplace offer get'] = """ +helps['edge disconnected-operation edge-marketplace offer get'] = """ type: command short-summary: Get details of a specific marketplace offer. examples: - name: Get details of a specific marketplace offer text: > - az disconnectedoperations edge-marketplace offer get --resource-group myResourceGroup --resource-name myResource --publisher-name publisherName --offer-name offerName + az edge disconnected-operation edge-marketplace offer get --resource-group myResourceGroup --resource-name myResource --publisher-name publisherName --offer-name offerName - name: Get offer details and output as JSON text: > - az disconnectedoperations edge-marketplace offer get -g myResourceGroup --resource-name myResource --publisher-name publisherName --offer-name offerName --output json + az edge disconnected-operation edge-marketplace offer get -g myResourceGroup --resource-name myResource --publisher-name publisherName --offer-name offerName --output json - name: Get offer details with custom query text: > - az disconnectedoperations edge-marketplace offer get -g myResourceGroup --resource-name myResource --publisher-name publisherName --offer-name offerName --query "[].{SKU:SKU,Version:Versions}" + az edge disconnected-operation edge-marketplace offer get -g myResourceGroup --resource-name myResource --publisher-name publisherName --offer-name offerName --query "[].{SKU:SKU,Version:Versions}" parameters: - name: --resource-group -g type: string @@ -70,14 +75,14 @@ short-summary: The name of the offer """ -helps['disconnectedoperations edge-marketplace offer package'] = """ +helps['edge disconnected-operation edge-marketplace offer package'] = """ type: command short-summary: Download and package a marketplace offer with its metadata and icons. long-summary: Downloads the marketplace offer metadata, icons, and creates a package in the specified output folder. examples: - name: Package a marketplace offer with specific version text: > - az disconnectedoperations edge-marketplace offer package --resource-group myResourceGroup --resource-name myResource --publisher-name publisherName --offer-name offerName --sku skuName --version versionNumber --output-folder "D:\\MarketplacePackages" + az edge disconnected-operation edge-marketplace offer package --resource-group myResourceGroup --resource-name myResource --publisher-name publisherName --offer-name offerName --sku skuName --version versionNumber --output-folder "D:\\MarketplacePackages" parameters: - name: --resource-group -g type: string diff --git a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py index f3ad496f601..e81396792b5 100644 --- a/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py +++ b/src/azure-cli/azure/cli/command_modules/disconnectedoperations/commands.py @@ -71,7 +71,7 @@ def load_command_table(self, _): # Register the parent command group with self.command_group( - "disconnectedoperations", + "edge disconnected-operation", custom_command_type=custom_command_type, is_preview=True, ) as g: @@ -79,7 +79,7 @@ def load_command_table(self, _): # Register the subgroup and its commands with self.command_group( - "disconnectedoperations edge-marketplace", + "edge disconnected-operation edge-marketplace", custom_command_type=custom_command_type, is_preview=True, ) as g: