Skip to content

Commit 8354b44

Browse files
authored
Add VC get and list operations (Azure#28624)
* VC operations
1 parent 614af57 commit 8354b44

File tree

14 files changed

+6766
-58
lines changed

14 files changed

+6766
-58
lines changed

sdk/ml/azure-ai-ml/azure/ai/ml/_ml_client.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
from azure.ai.ml._utils._experimental import experimental
4141
from azure.ai.ml._utils._http_utils import HttpPipeline
4242
from azure.ai.ml._utils._registry_utils import RegistryDiscovery
43-
from azure.ai.ml._utils.utils import _is_https_url, _validate_missing_sub_or_rg_and_raise
43+
from azure.ai.ml._utils.utils import _is_https_url
4444
from azure.ai.ml.constants._common import AzureMLResourceType
4545
from azure.ai.ml.entities import (
4646
BatchDeployment,
@@ -74,6 +74,7 @@
7474
RegistryOperations,
7575
WorkspaceConnectionsOperations,
7676
WorkspaceOperations,
77+
VirtualClusterOperations,
7778
)
7879
from azure.ai.ml.operations._code_operations import CodeOperations
7980
from azure.ai.ml.operations._local_deployment_helper import _LocalDeploymentHelper
@@ -158,8 +159,7 @@ def __init__(
158159
target=ErrorTarget.GENERAL,
159160
error_category=ErrorCategory.USER_ERROR,
160161
)
161-
if not registry_name:
162-
_validate_missing_sub_or_rg_and_raise(subscription_id, resource_group_name)
162+
163163
self._credential = credential
164164

165165
show_progress = kwargs.pop("show_progress", True)
@@ -429,6 +429,8 @@ def __init__(
429429
)
430430
self._operation_container.add(AzureMLResourceType.SCHEDULE, self._schedules)
431431

432+
self._virtual_clusters = VirtualClusterOperations(self._operation_scope, self._credential, **ops_kwargs)
433+
432434
@classmethod
433435
def from_config(
434436
cls,
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# ---------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# ---------------------------------------------------------
4+
5+
6+
from typing import List, Dict, Optional
7+
import azure.mgmt.resourcegraph as arg
8+
from azure.mgmt.resource import SubscriptionClient, ResourceManagementClient
9+
from azure.core.credentials import TokenCredential
10+
11+
12+
def get_resources_from_subscriptions(strQuery: str, credential: TokenCredential,
13+
subscription_list: Optional[List[str]] = None):
14+
15+
# If a subscription list is passed in, use it. Otherwise, get all subscriptions
16+
subsList = []
17+
if subscription_list is not None:
18+
subsList = subscription_list
19+
else:
20+
subsClient = SubscriptionClient(credential)
21+
for sub in subsClient.subscriptions.list():
22+
subsList.append(sub.as_dict().get('subscription_id'))
23+
24+
# Create Azure Resource Graph client and set options
25+
argClient = arg.ResourceGraphClient(credential)
26+
argQueryOptions = arg.models.QueryRequestOptions(result_format="objectArray")
27+
28+
# Create query
29+
argQuery = arg.models.QueryRequest(subscriptions=subsList, query=strQuery, options=argQueryOptions)
30+
31+
# Allowing API version to be set is yet to be released by azure-mgmt-resourcegraph,
32+
# hence the commented out code below. This is the API version Studio UX is using.
33+
# return argClient.resources(argQuery, api_version="2021-03-01")
34+
35+
return argClient.resources(argQuery)
36+
37+
38+
def get_virtual_clusters_from_subscriptions(credential: TokenCredential,
39+
subscription_list: Optional[List[str]] = None) -> List[Dict]:
40+
41+
# cspell:ignore tolower
42+
strQuery = """resources
43+
| where type == 'microsoft.machinelearningservices/virtualclusters'
44+
| order by tolower(name) asc
45+
| project id, subscriptionId, resourceGroup, name, location, tags, type"""
46+
47+
return get_resources_from_subscriptions(strQuery, credential, subscription_list).data
48+
49+
50+
def get_generic_resource_by_id(arm_id: str, credential: TokenCredential,
51+
subscription_id: str, api_version: Optional[str] = None) -> Dict:
52+
53+
resource_client = ResourceManagementClient(credential, subscription_id)
54+
generic_resource = resource_client.resources.get_by_id(arm_id, api_version)
55+
56+
return generic_resource.as_dict()
57+
58+
def get_virtual_cluster_by_id(name: str, resource_group: str,
59+
subscription_id: str, credential: TokenCredential) -> Dict:
60+
61+
arm_id = (
62+
f"/subscriptions/{subscription_id}/resourceGroups/{resource_group}"
63+
f"/providers/Microsoft.MachineLearningServices/virtualClusters/{name}"
64+
)
65+
66+
# This is the API version Studio UX is using.
67+
return get_generic_resource_by_id(arm_id, credential, subscription_id, api_version="2021-03-01-preview")

sdk/ml/azure-ai-ml/azure/ai/ml/_utils/utils.py

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -908,32 +908,6 @@ def get_all_enum_values_iter(enum_type):
908908
yield getattr(enum_type, key)
909909

910910

911-
def _validate_missing_sub_or_rg_and_raise(subscription_id: Optional[str], resource_group: Optional[str]):
912-
"""Determine if subscription or resource group is missing and raise exception
913-
as appropriate."""
914-
from azure.ai.ml.exceptions import ErrorCategory, ErrorTarget, ValidationException
915-
916-
# These imports can't be placed in at top file level because it will cause a circular import in
917-
# exceptions.py via _get_mfe_url_override
918-
919-
msg = "Both subscription id and resource group are required for this operation, missing {}"
920-
sub_msg = None
921-
if not subscription_id and not resource_group:
922-
sub_msg = "subscription id and resource group"
923-
elif not subscription_id and resource_group:
924-
sub_msg = "subscription id"
925-
elif subscription_id and not resource_group:
926-
sub_msg = "resource group"
927-
928-
if sub_msg:
929-
raise ValidationException(
930-
message=msg.format(sub_msg),
931-
no_personal_data_message=msg.format(sub_msg),
932-
target=ErrorTarget.GENERAL,
933-
error_category=ErrorCategory.USER_ERROR,
934-
)
935-
936-
937911
def write_to_shared_file(file_path: Union[str, PathLike], content: str):
938912
"""Open file with specific mode and return the file object.
939913

sdk/ml/azure-ai-ml/azure/ai/ml/operations/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from ._schedule_operations import ScheduleOperations
2424
from ._workspace_connections_operations import WorkspaceConnectionsOperations
2525
from ._workspace_operations import WorkspaceOperations
26+
from ._virtual_cluster_operations import VirtualClusterOperations
2627

2728
__all__ = [
2829
"ComputeOperations",
@@ -41,4 +42,5 @@
4142
"WorkspaceConnectionsOperations",
4243
"RegistryOperations",
4344
"ScheduleOperations",
45+
"VirtualClusterOperations",
4446
]
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# ---------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# ---------------------------------------------------------
4+
5+
from typing import Dict, Iterable, Optional
6+
7+
from azure.ai.ml._scope_dependent_operations import OperationScope
8+
9+
# from azure.ai.ml._telemetry import ActivityType, monitor_with_activity
10+
from azure.ai.ml._utils._logger_utils import OpsLogger
11+
from azure.ai.ml._utils.azure_resource_utils import get_virtual_clusters_from_subscriptions, get_virtual_cluster_by_id
12+
from azure.ai.ml.constants._common import Scope
13+
from azure.ai.ml.exceptions import UserErrorException
14+
from azure.core.tracing.decorator import distributed_trace
15+
from azure.core.credentials import TokenCredential
16+
17+
ops_logger = OpsLogger(__name__)
18+
module_logger = ops_logger.module_logger
19+
20+
21+
class VirtualClusterOperations():
22+
"""VirtualClusterOperations.
23+
24+
You should not instantiate this class directly. Instead, you should
25+
create an MLClient instance that instantiates it for you and
26+
attaches it as an attribute.
27+
"""
28+
29+
def __init__(
30+
self,
31+
operation_scope: OperationScope,
32+
credentials: TokenCredential,
33+
**kwargs: Dict,
34+
):
35+
self._resource_group_name = operation_scope.resource_group_name
36+
self._subscription_id = operation_scope.subscription_id
37+
self._credentials = credentials
38+
self._init_kwargs = kwargs
39+
40+
@distributed_trace
41+
# @monitor_with_activity(logger, "VirtualCluster.List", ActivityType.PUBLICAPI)
42+
def list(self, *, scope: Optional[str] = None) -> Iterable[Dict]:
43+
"""List virtual clusters a user has access to.
44+
45+
:param scope: scope of the listing, "subscription" or None, defaults to None.
46+
If None, list virtual clusters across all subscriptions a customer has access to.
47+
:type scope: str, optional
48+
:return: An iterator like instance of dictionaries.
49+
:rtype: ~azure.core.paging.ItemPaged[Dict]
50+
"""
51+
52+
if scope is None:
53+
subscription_list = None
54+
elif scope.lower() == Scope.SUBSCRIPTION:
55+
subscription_list = [self._subscription_id]
56+
else:
57+
message = f"Invalid scope: {scope}. Valid values are 'subscription' or None."
58+
raise UserErrorException(message=message, no_personal_data_message=message)
59+
60+
return get_virtual_clusters_from_subscriptions(self._credentials, subscription_list=subscription_list)
61+
62+
@distributed_trace
63+
# @monitor_with_activity(logger, "VirtualCluster.Get", ActivityType.PUBLICAPI)
64+
def get(self, name: str) -> Dict:
65+
"""Get a virtual cluster resource.
66+
67+
:param name: Name of the virtual cluster.
68+
:type name: str
69+
:return: Virtual cluster object
70+
:rtype: Dict
71+
"""
72+
73+
74+
return get_virtual_cluster_by_id(name=name, resource_group=self._resource_group_name,
75+
subscription_id=self._subscription_id, credential=self._credentials)

sdk/ml/azure-ai-ml/setup.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@
8585
"azure-common<2.0.0,>=1.1",
8686
"typing-extensions<5.0.0",
8787
# "opencensus-ext-azure<2.0.0", disabled until SDK logging re-activated
88+
"azure-mgmt-resourcegraph<9.0.0,>=2.0.0",
89+
"azure-mgmt-resource<23.0.0,>=3.0.0",
8890
],
8991
extras_require={
9092
# user can run `pip install azure-ai-ml[designer]` to install mldesigner alone with this package

sdk/ml/azure-ai-ml/tests/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -834,6 +834,7 @@ def pytest_configure(config):
834834
("data_experiences_test", "marks tests as data experience tests"),
835835
("local_endpoint_local_assets", "marks tests as local_endpoint_local_assets"),
836836
("local_endpoint_byoc", "marks tests as local_endpoint_byoc"),
837+
("virtual_cluster_test", "marks tests as virtual cluster tests"),
837838
]:
838839
config.addinivalue_line("markers", f"{marker}: {description}")
839840

sdk/ml/azure-ai-ml/tests/internal_utils/unittests/test_ml_client.py

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -421,35 +421,6 @@ def test_ml_client_with_invalid_cloud(self, mock_credential):
421421
assert ml_client._kwargs["cloud"] == "SomeInvalidCloudName"
422422
assert "Unknown cloud environment supplied" in str(e)
423423

424-
425-
def test_ml_client_validation_rg_sub_missing_throws(
426-
self, auth: ClientSecretCredential
427-
) -> None:
428-
with pytest.raises(ValidationException) as exception:
429-
MLClient(
430-
credential=auth,
431-
)
432-
message = exception.value.args[0]
433-
assert (
434-
message
435-
== "Both subscription id and resource group are required for this operation, missing subscription id and resource group"
436-
)
437-
438-
439-
def test_ml_client_with_no_rg_sub_for_ws_throws(
440-
self, e2e_ws_scope: OperationScope, auth: ClientSecretCredential
441-
) -> None:
442-
with pytest.raises(ValidationException) as exception:
443-
MLClient(
444-
credential=auth,
445-
workspace_name=e2e_ws_scope.workspace_name,
446-
)
447-
message = exception.value.args[0]
448-
assert (
449-
message
450-
== "Both subscription id and resource group are required for this operation, missing subscription id and resource group"
451-
)
452-
453424
def test_ml_client_with_both_workspace_registry_names_throws(self, e2e_ws_scope: OperationScope, auth: ClientSecretCredential) -> None:
454425
with pytest.raises(ValidationException) as exception:
455426
MLClient(

0 commit comments

Comments
 (0)