1717 BadRequestError ,
1818 CLIInternalError ,
1919 InvalidArgumentValueError ,
20+ MutuallyExclusiveArgumentError ,
2021 RequiredArgumentMissingError ,
2122 ResourceNotFoundError ,
2223 UnclassifiedUserFault ,
2930from knack .util import CLIError
3031
3132from azext_iot ._factory import iot_hub_service_factory , resource_service_factory
33+ from azext_iot .common ._azure import IOT_SERVICE_CS_TEMPLATE
34+ from azext_iot .constants import IOT_HUB_DEFAULT_POLICY
3235from azext_iot .common .certops import open_certificate
3336from azext_iot .core .shared import (
3437 ADR_CONFIGURE_ROLES_ERROR_MSG ,
4144 EndpointType ,
4245 IdentityType ,
4346 IotDpsSku ,
47+ IotHubAuthenticationType ,
4448 IotHubSku ,
4549 ManagedServiceIdentityType ,
4650 RenewKeyType ,
@@ -78,6 +82,13 @@ def _get_resource_group_from_hub(hub):
7882 return hub ["resourcegroup" ]
7983
8084
85+ def _resolve_linked_hub_hostname (hub , hostname_type = "device" ):
86+ """Resolve IoT Hub hostname for DPS linked hub based on hostname type."""
87+ if hostname_type == "device" :
88+ return hub ["properties" ].get ("deviceHostName" ) or hub ["properties" ]["hostName" ]
89+ return hub ["properties" ]["hostName" ]
90+
91+
8192# CUSTOM METHODS FOR DPS
8293def iot_dps_list (client , resource_group_name = None ):
8394 if resource_group_name is None :
@@ -327,40 +338,110 @@ def iot_dps_linked_hub_create(
327338 connection_string = None ,
328339 location = None ,
329340 resource_group_name = None ,
341+ authentication_type = None ,
342+ user_assigned_identity = None ,
343+ hostname_type = "device" ,
330344 apply_allocation_policy = None ,
331345 allocation_weight = None ,
332346 no_wait = False
333347):
334- if not any ([connection_string , hub_name ]):
335- raise RequiredArgumentMissingError ("Please provide the IoT Hub name or connection string." )
336- if not connection_string :
337- # Get the connection string for the hub
338- hub_client = iot_hub_service_factory (cmd .cli_ctx )
339- connection_string = iot_hub_show_connection_string (
340- hub_client , hub_name = hub_name , resource_group_name = hub_resource_group
341- )['connectionString' ]
348+ is_mi = authentication_type in (
349+ IotHubAuthenticationType .SYSTEM_ASSIGNED .value ,
350+ IotHubAuthenticationType .USER_ASSIGNED .value ,
351+ )
342352
343- if not location :
344- # Parse out hub name from connection string if needed
353+ # MI based Hub Linking in DPS
354+ if is_mi :
355+ if connection_string :
356+ raise MutuallyExclusiveArgumentError (
357+ "--connection-string cannot be used with --authentication-type. "
358+ "Use --hub-name instead for managed identity authentication."
359+ )
345360 if not hub_name :
346- try :
347- hub_name = re .search (r"hostname=(.[^\;\.]+)?" , connection_string , re .IGNORECASE ).group (1 )
348- except AttributeError :
349- raise InvalidArgumentValueError ("Please provide a valid IoT Hub connection string." )
361+ raise RequiredArgumentMissingError (
362+ "Please provide --hub-name for managed identity authentication."
363+ )
364+ if authentication_type == IotHubAuthenticationType .USER_ASSIGNED .value and not user_assigned_identity :
365+ raise RequiredArgumentMissingError (
366+ "--user-assigned-identity is required when --authentication-type is UserAssigned."
367+ )
350368
351369 hub_client = iot_hub_service_factory (cmd .cli_ctx )
352- try :
353- location = iot_hub_get (cmd , hub_client , hub_name = hub_name , resource_group_name = hub_resource_group )["location" ]
354- except CLIError :
355- raise RequiredArgumentMissingError ("Please provide the IoT Hub location." )
370+ hub = iot_hub_get (cmd , hub_client , hub_name = hub_name , resource_group_name = hub_resource_group )
371+ host_name = _resolve_linked_hub_hostname (hub , hostname_type )
372+
373+ # Validate MI is enabled on DPS
374+ resource_group_name = _ensure_dps_resource_group_name (client , resource_group_name , dps_name )
375+ dps = iot_dps_get (client , dps_name , resource_group_name )
376+ identity = dps .get ("identity" ) or {}
377+ identity_type = identity .get ("type" , "None" ) if isinstance (identity , dict ) else "None"
378+ if authentication_type == IotHubAuthenticationType .SYSTEM_ASSIGNED .value and "SystemAssigned" not in identity_type :
379+ raise InvalidArgumentValueError (
380+ f"System-assigned managed identity is not enabled on DPS '{ dps_name } '. "
381+ "Please enable it before linking with SystemAssigned authentication."
382+ )
383+ if authentication_type == IotHubAuthenticationType .USER_ASSIGNED .value and "UserAssigned" not in identity_type :
384+ raise InvalidArgumentValueError (
385+ f"User-assigned managed identity is not configured on DPS '{ dps_name } '. "
386+ "Please assign a user identity before linking with UserAssigned authentication."
387+ )
356388
357- resource_group_name = _ensure_dps_resource_group_name (client , resource_group_name , dps_name )
389+ linked_hub_entry = {
390+ "location" : location or hub ["location" ],
391+ "authenticationType" : authentication_type ,
392+ "hostName" : host_name ,
393+ }
394+ if user_assigned_identity :
395+ linked_hub_entry ["selectedUserAssignedIdentityResourceId" ] = user_assigned_identity
358396
359- dps = iot_dps_get (client , dps_name , resource_group_name )
360- dps ["properties" ]["iotHubs" ].append ({"connectionString" : connection_string ,
361- "location" : location ,
362- "applyAllocationPolicy" : apply_allocation_policy ,
363- "allocationWeight" : allocation_weight })
397+ # KeyBased Hub Linking in DPS
398+ else :
399+ if not any ([connection_string , hub_name ]):
400+ raise RequiredArgumentMissingError ("Please provide the IoT Hub name or connection string." )
401+ if not connection_string :
402+ hub_client = iot_hub_service_factory (cmd .cli_ctx )
403+ hub = iot_hub_get (cmd , hub_client , hub_name = hub_name , resource_group_name = hub_resource_group )
404+ host_name = _resolve_linked_hub_hostname (hub , hostname_type )
405+ location = location or hub ["location" ]
406+ # Build connection string with resolved hostname
407+ policies = iot_hub_policy_get (hub_client , hub_name , IOT_HUB_DEFAULT_POLICY ,
408+ _get_resource_group_from_hub (hub ))
409+ connection_string = IOT_SERVICE_CS_TEMPLATE .format (
410+ host_name , policies ["keyName" ], policies ["primaryKey" ]
411+ )
412+ else :
413+ if ".service.azure-devices" in connection_string .lower ():
414+ raise InvalidArgumentValueError (
415+ "Service hostname is not supported for DPS hub linking. "
416+ "Use a connection string with device or classic hostname."
417+ )
418+ if not location :
419+ if not hub_name :
420+ try :
421+ hub_name = re .search (r"hostname=(.[^\;\.]+)?" , connection_string , re .IGNORECASE ).group (1 )
422+ except AttributeError :
423+ raise InvalidArgumentValueError ("Please provide a valid IoT Hub connection string." )
424+
425+ hub_client = iot_hub_service_factory (cmd .cli_ctx )
426+ try :
427+ location = iot_hub_get (cmd , hub_client , hub_name = hub_name , resource_group_name = hub_resource_group )["location" ]
428+ except CLIError :
429+ raise RequiredArgumentMissingError ("Please provide the IoT Hub location." )
430+
431+ resource_group_name = _ensure_dps_resource_group_name (client , resource_group_name , dps_name )
432+ dps = iot_dps_get (client , dps_name , resource_group_name )
433+
434+ linked_hub_entry = {
435+ "connectionString" : connection_string ,
436+ "location" : location ,
437+ }
438+
439+ if apply_allocation_policy is not None :
440+ linked_hub_entry ["applyAllocationPolicy" ] = apply_allocation_policy
441+ if allocation_weight is not None :
442+ linked_hub_entry ["allocationWeight" ] = allocation_weight
443+
444+ dps ["properties" ]["iotHubs" ].append (linked_hub_entry )
364445
365446 if no_wait :
366447 return client .iot_dps_resource .begin_create_or_update (
@@ -374,8 +455,8 @@ def iot_dps_linked_hub_create(
374455 return iot_dps_linked_hub_list (client , dps_name , resource_group_name )
375456
376457
377- def iot_dps_linked_hub_update (cmd , client , dps_name , linked_hub , resource_group_name = None , apply_allocation_policy = None ,
378- allocation_weight = None , no_wait = False ):
458+ def iot_dps_linked_hub_update (cmd , client , dps_name , linked_hub , resource_group_name = None ,
459+ apply_allocation_policy = None , allocation_weight = None , no_wait = False ):
379460 if '.' not in linked_hub :
380461 hub_client = iot_hub_service_factory (cmd .cli_ctx )
381462 linked_hub = _get_iot_hub_hostname (hub_client , linked_hub )
0 commit comments