diff --git a/src/machinelearningservices/CHANGELOG.rst b/src/machinelearningservices/CHANGELOG.rst index a9d384b386a..567af97f4b9 100644 --- a/src/machinelearningservices/CHANGELOG.rst +++ b/src/machinelearningservices/CHANGELOG.rst @@ -1,3 +1,19 @@ +## 2025-11-04 + +### Azure Machine Learning CLI (v2) v 2.40.0 +- `az ml deployment-template create` + - Create a new deployment template from a YAML file. +- `az ml deployment-template list` + - List deployment templates in a registry +- `az ml deployment-template get` + - Get a specific deployment template by name and version. +- `az ml deployment-template update` + - Update specific fields of an existing deployment template. +- `az ml deployment-template archive` + - Archive a deployment template. +- `az ml deployment-template restore` + - Restore an archived deployment template. + ## 2025-08-27 ### Azure Machine Learning CLI (v2) v 2.39.0 diff --git a/src/machinelearningservices/azext_mlv2/manual/_help/__init__.py b/src/machinelearningservices/azext_mlv2/manual/_help/__init__.py index d4fe7e35d2c..727f17f36ba 100644 --- a/src/machinelearningservices/azext_mlv2/manual/_help/__init__.py +++ b/src/machinelearningservices/azext_mlv2/manual/_help/__init__.py @@ -19,6 +19,7 @@ from ._connection_help import get_connection_help from ._data_help import get_data_help from ._datastore_help import get_datastore_help +from ._deployment_template_help import get_deployment_template_help from ._environment_help import get_environment_help from ._feature_set_help import get_feature_set_help from ._feature_store_entity_help import get_feature_store_entity_help @@ -50,6 +51,7 @@ get_workspace_help() get_workspace_outbound_rule_help() get_datastore_help() +get_deployment_template_help() get_component_help() get_connection_help() get_schedule_help() diff --git a/src/machinelearningservices/azext_mlv2/manual/_help/_deployment_template_help.py b/src/machinelearningservices/azext_mlv2/manual/_help/_deployment_template_help.py new file mode 100644 index 00000000000..6cef35c9589 --- /dev/null +++ b/src/machinelearningservices/azext_mlv2/manual/_help/_deployment_template_help.py @@ -0,0 +1,106 @@ +# --------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# --------------------------------------------------------- + +from knack.help_files import helps + + +def get_deployment_template_help(): + """Load deployment template help content.""" + + +helps['ml deployment-template'] = """ +type: group +short-summary: Manage Azure ML deployment templates. +long-summary: | + Deployment templates are reusable templates that define deployment configurations for Azure ML. + They support registry-based operations only (not workspace-based) and provide a way to + standardize and share deployment configurations across teams and projects. +""" + +helps['ml deployment-template list'] = """ +type: command +short-summary: List deployment templates in a registry. +long-summary: | + List all deployment templates available in the specified registry. This command + returns all templates along with their metadata including name, version, description, and tags. +examples: + - name: List all deployment templates in a registry + text: az ml deployment-template list --registry-name myregistry + - name: List deployment templates with specific output format + text: az ml deployment-template list --registry-name myregistry --output table +""" + +helps['ml deployment-template get'] = """ +type: command +short-summary: Get a specific deployment template by name and version. +long-summary: | + Retrieve detailed information about a specific deployment template. If version is not + specified, the latest version will be returned. +examples: + - name: Get a specific version of a deployment template + text: az ml deployment-template get --name my-template --version 1 --registry-name myregistry +""" + +helps['ml deployment-template create'] = """ +type: command +short-summary: Create a new deployment template from a YAML file. +long-summary: | + Create a new deployment template using a YAML configuration file. The YAML file should + contain the complete deployment template definition including endpoints, parameters, and metadata. + You can override specific values using command-line parameters. +examples: + - name: Create a deployment template from a YAML file + text: az ml deployment-template create --file template.yml --registry-name myregistry + - name: Create with name and version overrides + text: az ml deployment-template create --file template.yml --name custom-template --version 2 --registry-name myregistry + - name: Create without waiting for completion + text: az ml deployment-template create --file template.yml --registry-name myregistry --no-wait +""" + +helps['ml deployment-template update'] = """ +type: command +short-summary: Update specific fields of an existing deployment template. +long-summary: | + Update metadata fields (description and tags) of an existing deployment template without + requiring a YAML file. This command follows Azure CLI conventions and only accepts specific + field updates. Tags are merged with existing tags rather than replaced. + + For structural changes to the deployment template (endpoints, deployment configuration, etc.), + use the 'create' command with a YAML file. +examples: + - name: Update deployment template description + text: az ml deployment-template update --name my-template --version 1 --registry-name myregistry --set "description=Updated description" + - name: Update deployment template tags + text: az ml deployment-template update --name my-template --version 1 --registry-name myregistry --set "tags=environment=production owner=ml-team" + - name: Update both description and tags + text: az ml deployment-template update --name my-template --version 1 --registry-name myregistry --set "description=Production template" --set "tags=status=active" +""" + +helps['ml deployment-template archive'] = """ +type: command +short-summary: Archive a deployment template. +long-summary: | + Archive a deployment template to mark it as inactive. Archived templates are not + returned in list operations by default. You can archive a specific version or all + versions of a template. +examples: + - name: Archive a specific version + text: az ml deployment-template archive --name my-template --version 1 --registry-name myregistry + - name: Archive without waiting for completion + text: az ml deployment-template archive --name my-template --version 1 --registry-name myregistry --no-wait +""" + +helps['ml deployment-template restore'] = """ +type: command +short-summary: Restore an archived deployment template. +long-summary: | + Restore a previously archived deployment template to make it active again. Restored + templates will appear in list operations. You can restore a specific version or all + versions of a template. +examples: + - name: Restore a specific version + text: az ml deployment-template restore --name my-template --version 1 --registry-name myregistry + - name: Restore without waiting for completion + text: az ml deployment-template restore --name my-template --version 1 --registry-name myregistry --no-wait +""" diff --git a/src/machinelearningservices/azext_mlv2/manual/_params/__init__.py b/src/machinelearningservices/azext_mlv2/manual/_params/__init__.py index 302b8630c17..da9df47459f 100644 --- a/src/machinelearningservices/azext_mlv2/manual/_params/__init__.py +++ b/src/machinelearningservices/azext_mlv2/manual/_params/__init__.py @@ -16,6 +16,7 @@ from ._connection_params import load_connection_params from ._data_params import load_data_params from ._datastore_params import load_datastore_params +from ._deployment_template_params import load_deployment_template_params from ._environment_params import load_environment_params from ._feature_set_params import load_feature_set_params from ._feature_store_entity_params import load_feature_store_entity_params @@ -44,6 +45,7 @@ def load_arguments(self, _): load_batch_endpoint_params(self) load_online_deployment_params(self) load_batch_deployment_params(self) + load_deployment_template_params(self) load_environment_params(self) load_compute_params(self) load_workspace_params(self) diff --git a/src/machinelearningservices/azext_mlv2/manual/_params/_deployment_template_params.py b/src/machinelearningservices/azext_mlv2/manual/_params/_deployment_template_params.py new file mode 100644 index 00000000000..8fe54f79345 --- /dev/null +++ b/src/machinelearningservices/azext_mlv2/manual/_params/_deployment_template_params.py @@ -0,0 +1,111 @@ +# --------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# --------------------------------------------------------- + +from ._common_params import ( + add_common_params, + add_description_param, + add_file_param, + add_lro_param, + add_override_param, + add_tags_param, +) + + +def add_deployment_template_common_param( + c, + name_help_message="Name of the deployment template.", + version_help_message="Version of the deployment template.", + name_required=True, + version_required=True, +): + c.argument("name", options_list=["--name", "-n"], help=name_help_message, required=name_required) + c.argument("version", options_list=["--version", "-v"], help=version_help_message, required=version_required) + + +def load_deployment_template_params(self): + with self.argument_context("ml deployment-template list") as c: + add_common_params(c) + c.argument( + "registry_name", + options_list=["--registry-name", "-r"], + required=True, + help=( + "Name of the registry. This is required since deployment templates " + "only support registry-name and not workspace." + ), + ) + + with self.argument_context("ml deployment-template get") as c: + add_common_params(c) + add_deployment_template_common_param(c, name_required=True, version_required=True) + c.argument( + "registry_name", + options_list=["--registry-name", "-r"], + required=True, + help=( + "Name of the registry. This is required since deployment templates " + "only support registry-name and not workspace." + ), + ) + + with self.argument_context("ml deployment-template create") as c: + add_common_params(c) + # Optional for create since they can come from file + add_deployment_template_common_param(c, name_required=False, version_required=False) + add_lro_param(c) + add_file_param(c, "deployment-template", "https://aka.ms/ml-cli-v2-deployment-template-yaml") + add_override_param(c) + c.argument( + "registry_name", + options_list=["--registry-name", "-r"], + required=True, + help=( + "Name of the registry. This is required since deployment templates " + "only support registry-name and not workspace." + ), + ) + + with self.argument_context("ml deployment-template update") as c: + add_common_params(c) + add_deployment_template_common_param(c, name_required=True, version_required=True) + add_override_param(c) + add_description_param(c, help_message="Description of the deployment template.") + add_tags_param(c) + c.argument( + "registry_name", + options_list=["--registry-name", "-r"], + required=True, + help=( + "Name of the registry. This is required since deployment templates " + "only support registry-name and not workspace." + ), + ) + + with self.argument_context("ml deployment-template archive") as c: + add_common_params(c) + add_deployment_template_common_param(c, name_required=True, version_required=True) + add_lro_param(c) + c.argument( + "registry_name", + options_list=["--registry-name", "-r"], + required=True, + help=( + "Name of the registry. This is required since deployment templates " + "only support registry-name and not workspace." + ), + ) + + with self.argument_context("ml deployment-template restore") as c: + add_common_params(c) + add_deployment_template_common_param(c, name_required=True, version_required=True) + add_lro_param(c) + c.argument( + "registry_name", + options_list=["--registry-name", "-r"], + required=True, + help=( + "Name of the registry. This is required since deployment templates " + "only support registry-name and not workspace." + ), + ) diff --git a/src/machinelearningservices/azext_mlv2/manual/commands.py b/src/machinelearningservices/azext_mlv2/manual/commands.py index 1d8e6f6ea7a..6a4bf0726a0 100644 --- a/src/machinelearningservices/azext_mlv2/manual/commands.py +++ b/src/machinelearningservices/azext_mlv2/manual/commands.py @@ -226,6 +226,25 @@ def load_command_table(self, _): supports_no_wait=True, ) + with self.command_group("ml deployment-template", client_factory=cf_ml_cl) as g: + custom_tmpl = "azext_mlv2.manual.custom.deployment_template#{}" + custom_deployment_template = CliCommandType(operations_tmpl=custom_tmpl) + g.custom_command("list", "ml_deployment_template_list", command_type=custom_deployment_template) + g.custom_command("get", "ml_deployment_template_get", command_type=custom_deployment_template) + g.custom_command("create", "ml_deployment_template_create", supports_no_wait=True, + command_type=custom_deployment_template) + g.generic_update_command( + "update", + getter_name="ml_deployment_template_get", + getter_type=custom_deployment_template, + setter_name="_ml_deployment_template_update", + setter_type=custom_deployment_template, + ) + g.custom_command("archive", "ml_deployment_template_archive", supports_no_wait=True, + command_type=custom_deployment_template) + g.custom_command("restore", "ml_deployment_template_restore", supports_no_wait=True, + command_type=custom_deployment_template) + with self.command_group("ml compute", client_factory=cf_ml_cl) as g: custom_tmpl = "azext_mlv2.manual.custom.compute#{}" custom_compute = CliCommandType(operations_tmpl=custom_tmpl) diff --git a/src/machinelearningservices/azext_mlv2/manual/custom/__init__.py b/src/machinelearningservices/azext_mlv2/manual/custom/__init__.py index 1e8413a51f4..ee2ddfa3370 100644 --- a/src/machinelearningservices/azext_mlv2/manual/custom/__init__.py +++ b/src/machinelearningservices/azext_mlv2/manual/custom/__init__.py @@ -36,6 +36,7 @@ from .connection import * # pylint: disable=wrong-import-position from .data import * # pylint: disable=wrong-import-position from .datastore import * # pylint: disable=wrong-import-position +from .deployment_template import * # pylint: disable=wrong-import-position from .environment import * # pylint: disable=wrong-import-position from .feature_set import * # pylint: disable=wrong-import-position from .feature_store import * # pylint: disable=wrong-import-position,unused-wildcard-import diff --git a/src/machinelearningservices/azext_mlv2/manual/custom/deployment_template.py b/src/machinelearningservices/azext_mlv2/manual/custom/deployment_template.py new file mode 100644 index 00000000000..cd9ed839213 --- /dev/null +++ b/src/machinelearningservices/azext_mlv2/manual/custom/deployment_template.py @@ -0,0 +1,220 @@ +# --------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# --------------------------------------------------------- + +import logging +from typing import Dict + +try: + from azure.ai.ml.entities._load_functions import load_deployment_template +except ImportError: + load_deployment_template = None + +from .raise_error import log_and_raise_error +from .utils import ( + get_ml_client, + is_not_found_error, + wrap_lro, +) + +module_logger = logging.getLogger(__name__) +module_logger.propagate = 0 +logger = logging.getLogger(__name__) + + +def ml_deployment_template_list(cmd, registry_name=None): + """List deployment templates in a registry.""" + ml_client, debug = get_ml_client( + cli_ctx=cmd.cli_ctx, registry_name=registry_name + ) + + try: + deployment_templates = ml_client.deployment_templates.list() + # Handle DeploymentTemplate serialization - try as_dict() first, then _to_dict() + result = [] + for template in deployment_templates: + try: + if hasattr(template, 'as_dict'): + result.append(template.as_dict()) + elif hasattr(template, '_to_dict'): + result.append(template._to_dict()) # pylint: disable=protected-access + else: + # Fallback to dict conversion + result.append(dict(template)) + except Exception as serialize_err: # pylint: disable=broad-except + module_logger.warning("Failed to serialize deployment template: %s", serialize_err) + result.append(str(template)) + return result + except Exception as err: # pylint: disable=broad-except + log_and_raise_error(err, debug) + + +def ml_deployment_template_get(cmd, name, version=None, registry_name=None): + """Get a specific deployment template by name and version.""" + ml_client, debug = get_ml_client( + cli_ctx=cmd.cli_ctx, registry_name=registry_name + ) + + try: + deployment_template = ml_client.deployment_templates.get(name=name, version=version) + # Handle DeploymentTemplate serialization + if hasattr(deployment_template, 'as_dict'): + return deployment_template.as_dict() + if hasattr(deployment_template, '_to_dict'): + return deployment_template._to_dict() # pylint: disable=protected-access + return dict(deployment_template) + except Exception as err: # pylint: disable=broad-except + if is_not_found_error(err): + raise ValueError(f"Deployment template '{name}' with version '{version}' does not exist.") from err + log_and_raise_error(err, debug) + + +def ml_deployment_template_create( + cmd, + file=None, + name=None, + version=None, + registry_name=None, + no_wait=False, + params_override=None, + **kwargs, # pylint: disable=unused-argument +): + """Create or update a deployment template.""" + ml_client, debug = get_ml_client( + cli_ctx=cmd.cli_ctx, registry_name=registry_name + ) + + params_override = params_override or [] + + try: + if name: + params_override.append({"name": name}) + if version: + params_override.append({"version": version}) + + if load_deployment_template: + deployment_template = load_deployment_template(source=file, params_override=params_override) + else: + # Fallback: load YAML manually if load_deployment_template is not available + import yaml + if not file: + raise ValueError("A YAML file must be provided for deployment template creation.") + + with open(file, 'r', encoding='utf-8') as f: + yaml_content = yaml.safe_load(f) + + # Apply parameter overrides + for override in params_override: + if isinstance(override, dict): + yaml_content.update(override) + + deployment_template = yaml_content + + deployment_template_result = ml_client.deployment_templates.create_or_update(deployment_template) + + if no_wait: + module_logger.warning( + "Deployment template create/update request initiated. " + "Status can be checked using `az ml deployment-template get -n %s -v %s`", + deployment_template.name if hasattr(deployment_template, 'name') else name or "unknown", + deployment_template.version if hasattr(deployment_template, 'version') else version or "unknown" + ) + return None + deployment_template_result = wrap_lro(cmd.cli_ctx, deployment_template_result) + + # Handle serialization + if hasattr(deployment_template_result, 'as_dict'): + return deployment_template_result.as_dict() + if hasattr(deployment_template_result, '_to_dict'): + return deployment_template_result._to_dict() # pylint: disable=protected-access + return dict(deployment_template_result) + except Exception as err: # pylint: disable=broad-except + yaml_operation = bool(file) + log_and_raise_error(err, debug, yaml_operation=yaml_operation) + + +def _ml_deployment_template_update( + cmd, registry_name=None, parameters: Dict = None +): + """Update function for generic_update_command pattern.""" + ml_client, debug = get_ml_client( + cli_ctx=cmd.cli_ctx, registry_name=registry_name + ) + + try: + deployment_template = ml_client.deployment_templates.get(name=parameters["name"], version=parameters["version"]) + + deployment_template.description = parameters["description"] + deployment_template.tags = parameters["tags"] + + deployment_template_result = ml_client.deployment_templates.create_or_update(deployment_template) + + # Handle serialization + if hasattr(deployment_template_result, 'as_dict'): + return deployment_template_result.as_dict() + if hasattr(deployment_template_result, '_to_dict'): + return deployment_template_result._to_dict() # pylint: disable=protected-access + return dict(deployment_template_result) + except Exception as err: # pylint: disable=broad-except + log_and_raise_error(err, debug) + + +def _ml_deployment_template_show(cmd, name, version=None, registry_name=None): + """Getter function for generic_update_command pattern.""" + ml_client, debug = get_ml_client( + cli_ctx=cmd.cli_ctx, registry_name=registry_name + ) + + try: + deployment_template = ml_client.deployment_templates.get(name=name, version=version) + + # Use to_rest_object to get proper field naming (snake_case instead of camelCase) + if hasattr(deployment_template, 'to_rest_object'): + return deployment_template.to_rest_object() + if hasattr(deployment_template, 'as_dict'): + return deployment_template.as_dict() + if hasattr(deployment_template, '_to_dict'): + return deployment_template._to_dict() # pylint: disable=protected-access + return dict(deployment_template) + except Exception as err: # pylint: disable=broad-except + if is_not_found_error(err): + raise ValueError(f"Deployment template '{name}' with version '{version}' does not exist.") from err + log_and_raise_error(err, debug) + + +def ml_deployment_template_archive( + cmd, + name, + version=None, + registry_name=None, + no_wait=False, + **kwargs, # pylint: disable=unused-argument +): + """Archive a deployment template.""" + ml_client, debug = get_ml_client( + cli_ctx=cmd.cli_ctx, registry_name=registry_name + ) + + try: + ml_client.deployment_templates.archive(name=name, version=version) + except Exception as err: # pylint: disable=broad-except + log_and_raise_error(err, debug) + + +def ml_deployment_template_restore( + cmd, + name, + version=None, + registry_name=None, + no_wait=False, + **kwargs, # pylint: disable=unused-argument +): + """Restore an archived deployment template.""" + ml_client, debug = get_ml_client( + cli_ctx=cmd.cli_ctx, registry_name=registry_name + ) + + try: + ml_client.deployment_templates.restore(name=name, version=version) + except Exception as err: # pylint: disable=broad-except + log_and_raise_error(err, debug)