Skip to content

Commit c3b7b30

Browse files
Adding in some functionality to use different clouds (Azure#28528)
* Adding in some functionality to use different clouds This support is needed for the air gapped environments. There are three ways to add a new cloud environment. These were all taken from examples in the v1 sdk. 1) The SDK will look for a default configuration file and try to find cloud environments in there. 2) If you set an environment variable called ARM_METADATA_URL, it will look there for cloud configurations. If you do not set this, it will use a default URL in the _azure_environments.py file to find them. 3) The SDK exposes two new functions, add_cloud which will add the new configuration to the configuration file mentioned in #1, and update_cloud which will update the added configuration. * Removing some of the functionality, only ARM check remains * Adding unit test for new environments functionality * fixed tests with mock * removed print statement * removed commented code * removed print statements (oops) * fixed tests and removed comments * Fixing a testing bug * Fixing a misspelled word * Changing how we reach out to ARM, als fixing some pylint * Fixing more lint errors * Fixing more lint errors * updated code per suggestions in PR * fixed typo in warning * added registry_endpoint to metadata url, also added tests for making sure all endpointurls are registered * updated how the registry discovery endpoint is created. Uses a default region but region can be updated with environment variable * fixed linting errors * moved discovery url logic around to make sure it's not overwriting public regions * Fixing small pylint errors * Moving over to using HttpPipeline instead of requests * fixed up based on comments in the PR * fixed broken unit tests and mocked correctly * Fixing pylint issues --------- Co-authored-by: Ronald Shaw <[email protected]>
1 parent 77ae304 commit c3b7b30

File tree

3 files changed

+180
-11
lines changed

3 files changed

+180
-11
lines changed

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

Lines changed: 97 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010

1111
from azure.ai.ml._utils.utils import _get_mfe_url_override
1212
from azure.ai.ml.constants._common import AZUREML_CLOUD_ENV_NAME
13+
from azure.ai.ml.constants._common import ArmConstants
14+
from azure.core.rest import HttpRequest
15+
from azure.mgmt.core import ARMPipelineClient
16+
17+
1318

1419
module_logger = logging.getLogger(__name__)
1520

@@ -56,6 +61,19 @@ class EndpointURLS: # pylint: disable=too-few-public-methods,no-init
5661
},
5762
}
5863

64+
_requests_pipeline = None
65+
66+
def _get_cloud(cloud: str):
67+
if cloud in _environments:
68+
return _environments[cloud]
69+
arm_url = os.environ.get(ArmConstants.METADATA_URL_ENV_NAME,ArmConstants.DEFAULT_URL)
70+
arm_clouds = _get_clouds_by_metadata_url(arm_url)
71+
try:
72+
new_cloud = arm_clouds[cloud]
73+
_environments.update(new_cloud)
74+
return new_cloud
75+
except KeyError:
76+
raise Exception('Unknown cloud environment "{0}".'.format(cloud))
5977

6078
def _get_default_cloud_name():
6179
"""Return AzureCloud as the default cloud."""
@@ -74,17 +92,18 @@ def _get_cloud_details(cloud: str = AzureEnvironments.ENV_DEFAULT):
7492
AzureEnvironments.ENV_DEFAULT,
7593
)
7694
cloud = _get_default_cloud_name()
77-
try:
78-
azure_environment = _environments[cloud]
79-
module_logger.debug("Using the cloud configuration: '%s'.", azure_environment)
80-
except KeyError:
81-
raise Exception('Unknown cloud environment "{0}".'.format(cloud))
82-
return azure_environment
95+
return _get_cloud(cloud)
8396

8497

8598
def _set_cloud(cloud: str = AzureEnvironments.ENV_DEFAULT):
99+
"""Sets the current cloud
100+
101+
:param cloud: cloud name
102+
"""
86103
if cloud is not None:
87-
if cloud not in _environments:
104+
try:
105+
_get_cloud(cloud)
106+
except Exception:
88107
raise Exception('Unknown cloud environment supplied: "{0}".'.format(cloud))
89108
else:
90109
cloud = _get_default_cloud_name()
@@ -189,3 +208,74 @@ def _resource_to_scopes(resource):
189208
"""
190209
scope = resource + "/.default"
191210
return [scope]
211+
212+
def _get_registry_discovery_url(cloud, cloud_suffix=""):
213+
"""Get or generate the registry discovery url
214+
215+
:param cloud: configuration of the cloud to get the registry_discovery_url from
216+
:param cloud_suffix: the suffix to use for the cloud, in the case that the registry_discovery_url
217+
must be generated
218+
:return: string of discovery url
219+
"""
220+
cloud_name = cloud["name"]
221+
if cloud_name in _environments:
222+
return _environments[cloud_name].registry_url
223+
224+
registry_discovery_region = os.environ.get(
225+
ArmConstants.REGISTRY_DISCOVERY_REGION_ENV_NAME,
226+
ArmConstants.REGISTRY_DISCOVERY_DEFAULT_REGION
227+
)
228+
registry_discovery_region_default = "https://{}{}.api.azureml.{}/".format(
229+
cloud_name.lower(),
230+
registry_discovery_region,
231+
cloud_suffix
232+
)
233+
return os.environ.get(ArmConstants.REGISTRY_ENV_URL, registry_discovery_region_default)
234+
235+
def _get_clouds_by_metadata_url(metadata_url):
236+
"""Get all the clouds by the specified metadata url
237+
238+
:return: list of the clouds
239+
"""
240+
try:
241+
module_logger.debug('Start : Loading cloud metadata from the url specified by %s', metadata_url)
242+
client = ARMPipelineClient(base_url=metadata_url, policies=[])
243+
HttpRequest("GET", metadata_url)
244+
with client.send_request(HttpRequest("GET", metadata_url)) as meta_response:
245+
arm_cloud_dict = meta_response.json()
246+
cli_cloud_dict = _convert_arm_to_cli(arm_cloud_dict)
247+
module_logger.debug('Finish : Loading cloud metadata from the url specified by %s', metadata_url)
248+
return cli_cloud_dict
249+
except Exception as ex: # pylint: disable=broad-except
250+
module_logger.warning("Error: Azure ML was unable to load cloud metadata from the url specified by %s. %s. "
251+
"This may be due to a misconfiguration of networking controls. Azure Machine Learning Python "
252+
"SDK requires outbound access to Azure Resource Manager. Please contact your networking team "
253+
"to configure outbound access to Azure Resource Manager on both Network Security Group and "
254+
"Firewall. For more details on required configurations, see "
255+
"https://docs.microsoft.com/azure/machine-learning/how-to-access-azureml-behind-firewall.",
256+
metadata_url, ex)
257+
return {}
258+
259+
def _convert_arm_to_cli(arm_cloud_metadata):
260+
cli_cloud_metadata_dict = {}
261+
if isinstance(arm_cloud_metadata, dict):
262+
arm_cloud_metadata = [arm_cloud_metadata]
263+
264+
for cloud in arm_cloud_metadata:
265+
try:
266+
cloud_name = cloud["name"]
267+
portal_endpoint = cloud["portal"]
268+
cloud_suffix = ".".join(portal_endpoint.split('.')[2:]).replace("/", "")
269+
registry_discovery_url = _get_registry_discovery_url(cloud, cloud_suffix)
270+
cli_cloud_metadata_dict[cloud_name] = {
271+
EndpointURLS.AZURE_PORTAL_ENDPOINT: cloud["portal"],
272+
EndpointURLS.RESOURCE_MANAGER_ENDPOINT: cloud["resourceManager"],
273+
EndpointURLS.ACTIVE_DIRECTORY_ENDPOINT: cloud["authentication"]["loginEndpoint"],
274+
EndpointURLS.AML_RESOURCE_ID: "https://ml.azure.{}".format(cloud_suffix),
275+
EndpointURLS.STORAGE_ENDPOINT: cloud["suffixes"]["storage"],
276+
EndpointURLS.REGISTRY_DISCOVERY_ENDPOINT: registry_discovery_url
277+
}
278+
except KeyError as ex:
279+
module_logger.warning("Property on cloud not found in arm cloud metadata: %s", ex)
280+
continue
281+
return cli_cloud_metadata_dict

sdk/ml/azure-ai-ml/azure/ai/ml/constants/_common.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,12 @@ class ArmConstants(object):
276276
AZURE_MGMT_KEYVAULT_API_VERSION = "2019-09-01"
277277
AZURE_MGMT_CONTAINER_REG_API_VERSION = "2019-05-01"
278278

279+
DEFAULT_URL = "https://management.azure.com/metadata/endpoints?api-version=2019-05-01"
280+
METADATA_URL_ENV_NAME = "ARM_CLOUD_METADATA_URL"
281+
REGISTRY_DISCOVERY_DEFAULT_REGION = "west"
282+
REGISTRY_DISCOVERY_REGION_ENV_NAME = "REGISTRY_DISCOVERY_ENDPOINT_REGION"
283+
REGISTRY_ENV_URL = "REGISTRY_DISCOVERY_ENDPOINT_URL"
284+
279285

280286
class HttpResponseStatusCode(object):
281287
NOT_FOUND = 404

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

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,66 @@
11
import os
2-
32
import mock
43
import pytest
4+
from mock import MagicMock, patch
55

66
from azure.ai.ml._azure_environments import (
77
AzureEnvironments,
8+
EndpointURLS,
89
_get_azure_portal_id_from_metadata,
910
_get_base_url_from_metadata,
11+
_get_cloud_details,
1012
_get_cloud_information_from_metadata,
1113
_get_default_cloud_name,
1214
_get_registry_discovery_endpoint_from_metadata,
1315
_get_storage_endpoint_from_metadata,
1416
_set_cloud,
1517
)
16-
from azure.ai.ml.constants._common import AZUREML_CLOUD_ENV_NAME
18+
from azure.ai.ml.constants._common import ArmConstants, AZUREML_CLOUD_ENV_NAME
19+
from azure.mgmt.core import ARMPipelineClient
20+
21+
def mocked_send_request_get(*args, **kwargs):
22+
class MockResponse:
23+
def __init__(self):
24+
self.status_code = 201
25+
def __enter__(self):
26+
return self
27+
def __exit__(self, exc_type, exc_value, traceback):
28+
return
29+
def json(self):
30+
return [
31+
{
32+
"name": "TEST_ENV",
33+
"portal": "testportal.azure.com",
34+
"resourceManager": "testresourcemanager.azure.com",
35+
"authentication": {
36+
"loginEndpoint": "testdirectoryendpoint.azure.com"
37+
},
38+
"suffixes": {
39+
"storage": "teststorageendpoint"
40+
}
41+
},
42+
{
43+
"name": "TEST_ENV2",
44+
"portal": "testportal.azure.windows.net",
45+
"resourceManager": "testresourcemanager.azure.com",
46+
"authentication": {
47+
"loginEndpoint": "testdirectoryendpoint.azure.com"
48+
},
49+
"suffixes": {
50+
"storage": "teststorageendpoint"
51+
}
52+
},
53+
{
54+
"name": "MISCONFIGURED"
55+
}
56+
]
57+
return MockResponse()
1758

1859

1960
@pytest.mark.unittest
2061
@pytest.mark.core_sdk_test
2162
class TestCloudEnvironments:
63+
2264
@mock.patch.dict(os.environ, {AZUREML_CLOUD_ENV_NAME: AzureEnvironments.ENV_DEFAULT}, clear=True)
2365
def test_set_valid_cloud_details_china(self):
2466
cloud_environment = AzureEnvironments.ENV_CHINA
@@ -70,7 +112,6 @@ def test_get_default_cloud(self):
70112
with mock.patch("os.environ", {AZUREML_CLOUD_ENV_NAME: "yadadada"}):
71113
cloud_name = _get_default_cloud_name()
72114
assert cloud_name == "yadadada"
73-
74115

75116
def test_get_registry_endpoint_from_public(self):
76117
cloud_environment = AzureEnvironments.ENV_DEFAULT
@@ -88,4 +129,36 @@ def test_get_registry_endpoint_from_us_gov(self):
88129
cloud_environment = AzureEnvironments.ENV_US_GOVERNMENT
89130
_set_cloud(cloud_environment)
90131
base_url = _get_registry_discovery_endpoint_from_metadata(cloud_environment)
91-
assert "https://usgovarizona.api.ml.azure.us/" in base_url
132+
assert "https://usgovarizona.api.ml.azure.us/" in base_url
133+
134+
@mock.patch.dict(os.environ, {}, clear=True)
135+
@mock.patch("azure.mgmt.core.ARMPipelineClient.send_request", side_effect=mocked_send_request_get)
136+
def test_get_cloud_from_arm(self, mock_arm_pipeline_client_send_request):
137+
138+
_set_cloud('TEST_ENV')
139+
cloud_details = _get_cloud_information_from_metadata("TEST_ENV")
140+
assert cloud_details.get("cloud") == "TEST_ENV"
141+
142+
@mock.patch.dict(os.environ, {}, clear=True)
143+
@mock.patch("azure.mgmt.core.ARMPipelineClient.send_request", side_effect=mocked_send_request_get)
144+
def test_all_endpointurls_used(self, mock_get):
145+
cloud_details = _get_cloud_details("TEST_ENV")
146+
endpoint_urls = [a for a in dir(EndpointURLS) if not a.startswith('__')]
147+
for url in endpoint_urls:
148+
try:
149+
cloud_details[EndpointURLS.__dict__[url]]
150+
except:
151+
assert False, "Url not found: {}".format(EndpointURLS.__dict__[url])
152+
assert True
153+
154+
@mock.patch.dict(os.environ, {}, clear=True)
155+
@mock.patch("azure.mgmt.core.ARMPipelineClient.send_request", side_effect=mocked_send_request_get)
156+
def test_metadata_registry_endpoint(self, mock_get):
157+
cloud_details = _get_cloud_details("TEST_ENV2")
158+
assert cloud_details.get(EndpointURLS.REGISTRY_DISCOVERY_ENDPOINT) == "https://test_env2west.api.azureml.windows.net/"
159+
160+
@mock.patch.dict(os.environ, {}, clear=True)
161+
@mock.patch("azure.mgmt.core.ARMPipelineClient.send_request", side_effect=mocked_send_request_get)
162+
def test_arm_misconfigured(self, mock_get):
163+
with pytest.raises(Exception) as e_info:
164+
_set_cloud("MISCONFIGURED")

0 commit comments

Comments
 (0)