4
4
5
5
"""Metadata to interact with different Azure clouds."""
6
6
7
+ import configparser
7
8
import logging
8
9
import os
10
+ import sys
9
11
from typing import Dict , Optional
10
12
11
13
from azure .ai .ml ._utils .utils import _get_mfe_url_override
@@ -75,7 +77,8 @@ def _get_cloud_details(cloud: str = AzureEnvironments.ENV_DEFAULT):
75
77
)
76
78
cloud = _get_default_cloud_name ()
77
79
try :
78
- azure_environment = _environments [cloud ]
80
+ all_clouds = _get_all_clouds ()
81
+ azure_environment = all_clouds [cloud ]
79
82
module_logger .debug ("Using the cloud configuration: '%s'." , azure_environment )
80
83
except KeyError :
81
84
raise Exception ('Unknown cloud environment "{0}".' .format (cloud ))
@@ -84,7 +87,7 @@ def _get_cloud_details(cloud: str = AzureEnvironments.ENV_DEFAULT):
84
87
85
88
def _set_cloud (cloud : str = AzureEnvironments .ENV_DEFAULT ):
86
89
if cloud is not None :
87
- if cloud not in _environments :
90
+ if cloud not in _get_all_clouds () :
88
91
raise Exception ('Unknown cloud environment supplied: "{0}".' .format (cloud ))
89
92
else :
90
93
cloud = _get_default_cloud_name ()
@@ -189,3 +192,130 @@ def _resource_to_scopes(resource):
189
192
"""
190
193
scope = resource + "/.default"
191
194
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 )
0 commit comments