Skip to content

Commit f5657de

Browse files
committed
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.
1 parent 698471b commit f5657de

File tree

3 files changed

+145
-2
lines changed

3 files changed

+145
-2
lines changed

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

+6
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232
load_workspace,
3333
load_workspace_connection,
3434
)
35+
from .entities._manage_clouds import (
36+
add_cloud,
37+
update_cloud,
38+
)
3539

3640
module_logger = logging.getLogger(__name__)
3741
initialize_logger_info(module_logger, terminator="\n")
@@ -63,6 +67,8 @@
6367
"load_workspace",
6468
"load_registry",
6569
"load_workspace_connection",
70+
"add_cloud",
71+
"update_cloud",
6672
]
6773

6874
__version__ = VERSION

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

+132-2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44

55
"""Metadata to interact with different Azure clouds."""
66

7+
import configparser
78
import logging
89
import os
10+
import sys
911
from typing import Dict, Optional
1012

1113
from azure.ai.ml._utils.utils import _get_mfe_url_override
@@ -75,7 +77,8 @@ def _get_cloud_details(cloud: str = AzureEnvironments.ENV_DEFAULT):
7577
)
7678
cloud = _get_default_cloud_name()
7779
try:
78-
azure_environment = _environments[cloud]
80+
all_clouds = _get_all_clouds()
81+
azure_environment = all_clouds[cloud]
7982
module_logger.debug("Using the cloud configuration: '%s'.", azure_environment)
8083
except KeyError:
8184
raise Exception('Unknown cloud environment "{0}".'.format(cloud))
@@ -84,7 +87,7 @@ def _get_cloud_details(cloud: str = AzureEnvironments.ENV_DEFAULT):
8487

8588
def _set_cloud(cloud: str = AzureEnvironments.ENV_DEFAULT):
8689
if cloud is not None:
87-
if cloud not in _environments:
90+
if cloud not in _get_all_clouds():
8891
raise Exception('Unknown cloud environment supplied: "{0}".'.format(cloud))
8992
else:
9093
cloud = _get_default_cloud_name()
@@ -189,3 +192,130 @@ def _resource_to_scopes(resource):
189192
"""
190193
scope = resource + "/.default"
191194
return [scope]
195+
196+
197+
_AZUREML_AUTH_CONFIG_DIR_ENV_NAME = 'AZUREML_AUTH_CONFIG_DIR'
198+
def _get_config_dir():
199+
"""Folder path for azureml-core to store authentication config"""
200+
_AUTH_FOLDER_PATH = os.path.expanduser(os.path.join('~', '.azureml', "auth"))
201+
if os.getenv(_AZUREML_AUTH_CONFIG_DIR_ENV_NAME, None):
202+
return os.getenv(_AZUREML_AUTH_CONFIG_DIR_ENV_NAME, None)
203+
else:
204+
if sys.version_info > (3, 0):
205+
os.makedirs(_AUTH_FOLDER_PATH, exist_ok=True)
206+
else:
207+
if not os.path.exists(_AUTH_FOLDER_PATH):
208+
os.makedirs(_AUTH_FOLDER_PATH)
209+
210+
return _AUTH_FOLDER_PATH
211+
212+
GLOBAL_CONFIG_DIR = _get_config_dir()
213+
CLOUD_CONFIG_FILE = os.path.join(GLOBAL_CONFIG_DIR, 'clouds.config')
214+
DEFAULT_TIMEOUT = 30
215+
_DEFAULT_ARM_URL = "https://management.azure.com/metadata/endpoints?api-version=2019-05-01"
216+
_ARM_METADATA_URL_ENV_NAME = "ARM_METADATA_URL"
217+
218+
def _get_clouds_by_metadata_url(metadata_url, timeout=DEFAULT_TIMEOUT):
219+
"""Get all the clouds by the specified metadata url
220+
221+
:return: list of the clouds
222+
"""
223+
try:
224+
import requests
225+
module_logger.debug('Start : Loading cloud metatdata from the url specified by {0}'.format(metadata_url))
226+
with requests.get(metadata_url, timeout=timeout) as meta_response:
227+
arm_cloud_dict = meta_response.json()
228+
cli_cloud_dict = _convert_arm_to_cli(arm_cloud_dict)
229+
module_logger.debug('Finish : Loading cloud metatdata from the url specified by {0}'.format(metadata_url))
230+
return cli_cloud_dict
231+
except Exception as ex: # pylint: disable=broad-except
232+
module_logger.warning("Error: Azure ML was unable to load cloud metadata from the url specified by {0}. {1}. "
233+
"This may be due to a misconfiguration of networking controls. Azure Machine Learning Python SDK "
234+
"requires outbound access to Azure Resource Manager. Please contact your networking team to configure "
235+
"outbound access to Azure Resource Manager on both Network Security Group and Firewall. "
236+
"For more details on required configurations, see "
237+
"https://docs.microsoft.com/azure/machine-learning/how-to-access-azureml-behind-firewall.".format(
238+
metadata_url, ex))
239+
240+
def _convert_arm_to_cli(arm_cloud_metadata_dict):
241+
cli_cloud_metadata_dict = {}
242+
for cloud in arm_cloud_metadata_dict:
243+
cli_cloud_metadata_dict[cloud['name']] = {
244+
EndpointURLS.AZURE_PORTAL_ENDPOINT: cloud["portal"],
245+
EndpointURLS.RESOURCE_MANAGER_ENDPOINT: cloud["resourceManager"],
246+
EndpointURLS.ACTIVE_DIRECTORY_ENDPOINT: cloud["authentication"]["loginEndpoint"],
247+
EndpointURLS.AML_RESOURCE_ID: cloud["resourceManager"],
248+
EndpointURLS.STORAGE_ENDPOINT: cloud["suffixes"]["storage"]
249+
}
250+
return cli_cloud_metadata_dict
251+
252+
def _get_cloud(cloud_name):
253+
return next((data for name,data in _get_all_clouds().items() if name == cloud_name), None)
254+
255+
256+
def _get_all_clouds():
257+
# Start with the hard coded list of clouds in this file
258+
all_clouds = {}
259+
all_clouds.update(_environments)
260+
# Get configs from the config file
261+
config = configparser.ConfigParser()
262+
try:
263+
config.read(CLOUD_CONFIG_FILE)
264+
except configparser.MissingSectionHeaderError:
265+
os.remove(CLOUD_CONFIG_FILE)
266+
module_logger.warning("'%s' is in bad format and has been removed.", CLOUD_CONFIG_FILE)
267+
for section in config.sections():
268+
all_clouds[section] = dict(config.items(section))
269+
# Now do the metadata URL
270+
arm_url = os.environ[_ARM_METADATA_URL_ENV_NAME] if _ARM_METADATA_URL_ENV_NAME in os.environ else _DEFAULT_ARM_URL
271+
all_clouds.update(_get_clouds_by_metadata_url(arm_url))
272+
# Send them all along with the hardcoded environments
273+
return all_clouds
274+
275+
class CloudNotRegisteredException(Exception):
276+
def __init__(self, cloud_name):
277+
super(CloudNotRegisteredException, self).__init__(cloud_name)
278+
self.cloud_name = cloud_name
279+
280+
def __str__(self):
281+
return "The cloud '{}' is not registered.".format(self.cloud_name)
282+
283+
284+
class CloudAlreadyRegisteredException(Exception):
285+
def __init__(self, cloud_name):
286+
super(CloudAlreadyRegisteredException, self).__init__(cloud_name)
287+
self.cloud_name = cloud_name
288+
289+
def __str__(self):
290+
return "The cloud '{}' is already registered.".format(self.cloud_name)
291+
292+
def _config_add_cloud(config, cloud, overwrite=False):
293+
""" Add a cloud to a config object """
294+
try:
295+
config.add_section(cloud["name"])
296+
except configparser.DuplicateSectionError:
297+
if not overwrite:
298+
raise CloudAlreadyRegisteredException(cloud["name"])
299+
for k,v in cloud.items():
300+
if k != "name" and v is not None:
301+
config.set(cloud["name"], k, v)
302+
303+
def _save_cloud(cloud, overwrite=False):
304+
config = configparser.ConfigParser()
305+
config.read(CLOUD_CONFIG_FILE)
306+
_config_add_cloud(config, cloud, overwrite=overwrite)
307+
if not os.path.isdir(GLOBAL_CONFIG_DIR):
308+
os.makedirs(GLOBAL_CONFIG_DIR)
309+
with open(CLOUD_CONFIG_FILE, 'w') as configfile:
310+
config.write(configfile)
311+
312+
def _add_cloud(cloud):
313+
if _get_cloud(cloud["name"]):
314+
raise CloudAlreadyRegisteredException(cloud["name"])
315+
_save_cloud(cloud)
316+
317+
318+
def _update_cloud(cloud):
319+
if not _get_cloud(cloud["name"]):
320+
raise CloudNotRegisteredException(cloud["name"])
321+
_save_cloud(cloud, overwrite=True)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from azure.ai.ml._azure_environments import (_add_cloud, _update_cloud)
2+
3+
def add_cloud(cloud):
4+
_add_cloud(cloud)
5+
6+
def update_cloud(cloud):
7+
_update_cloud(cloud)

0 commit comments

Comments
 (0)