Skip to content
Closed
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions src/azure-cli/azure/cli/command_modules/hdinsight/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,36 @@
az hdinsight create -t spark --version 3.6 -g MyResourceGroup -n MyCluster \\
-p "HttpPassword1234!" --storage-account MyStorageAccount \\
--enable-compute-isolation --workernode-size "Standard_E8S_V3" --headnode-size "Standard_E8S_V3"
- name: Create a cluster with WASB + MSI.
text: |-
az hdinsight create -t spark -g MyResourceGroup -n MyCluster \\
-p "HttpPassword1234!" \\
--storage-account MyStorageAccount \\
--storage-account-managed-identity MyMSI
- name: Create a entra user cluster with Entra User By ObjectId Or Upn (comma-separated)
text: |-
az hdinsight create -t spark -g MyResourceGroup -n MyCluster \\
--ssh-password "sshPassword1234!" \\
--storage-account MyStorageAccount \\
--entra-user-identity "objectId1","objectId2","upn3"
- name: Create a entra user cluster with Entra User By ObjectId Or Upn (space-separated)
text: |-
az hdinsight create -t spark -g MyResourceGroup -n MyCluster \\
--ssh-password "sshPassword1234!" \\
--storage-account MyStorageAccount \\
--entra-user-identity "objectId1" "objectId2" "upn3"
- name: Create a entra user cluster with Entra User By a JSON string
text: |-
az hdinsight create -t spark -g MyResourceGroup -n MyCluster \\
--ssh-password "sshPassword1234!" \\
--storage-account MyStorageAccount \\
--entra-user-full-info '[{\"objectID\": \"00000000-0000-0000-0000-000000000000\",\"displayName\": \"displayName\",\"upn\": \"user@contoso.com\"}]'
- name: Create a entra user cluster with Entra User By a JSON file
text: |-
az hdinsight create -t spark -g MyResourceGroup -n MyCluster \\
--ssh-password "sshPassword1234!" \\
--storage-account MyStorageAccount \\
--entra-user-full-info @config.json
"""

helps['hdinsight resize'] = """
Expand Down Expand Up @@ -434,3 +464,34 @@
type: command
short-summary: Place the CLI in a waiting state until an operation is complete.
"""

helps['hdinsight credentials update'] = """
type: command
short-summary: Update credentials for an existing HDInsight cluster, including Entra ID users and HTTP password.
examples:
- name: Update Entra ID users by object ID or UPN (comma-separated)
text: |-
az hdinsight credentials update --name MyCluster --resource-group rg \\
--entra-user-identity "objectId1","objectId2","upn3"
- name: Update Entra ID users by object ID or UPN (space-separated)
text: |-
az hdinsight credentials update --name MyCluster --resource-group rg \\
--entra-user-identity "objectId1" "objectId2" "upn3"
- name: Update Entra ID users using a JSON string
text: |-
az hdinsight credentials update --name MyCluster --resource-group rg \\
--entra-user-full-info '[{\"objectID\": \"00000000-0000-0000-0000-000000000000\",\"displayName\": \"displayName\",\"upn\": \"user@contoso.com\"}]'
- name: Update Entra ID users using a JSON file
text: |-
az hdinsight credentials update --name MyCluster --resource-group rg \\
--entra-user-full-info @config.json
- name: Update the HTTP password for the cluster
text: |-
az hdinsight credentials update --name MyCluster --resource-group rg \\
--http-password "HttpPassword1234!"
"""

helps['hdinsight credentials show'] = """
type: command
short-summary: Show credential configuration of an existing HDInsight cluster, including HTTP username, password, and Entra ID user settings
"""
21 changes: 21 additions & 0 deletions src/azure-cli/azure/cli/command_modules/hdinsight/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,13 @@ def load_arguments(self, _):
help='HTTP username for the cluster. Default: admin.')
c.argument('http_password', options_list=['--http-password', '-p'], arg_group='HTTP',
help='HTTP password for the cluster. Will prompt if not given.')
c.argument('entra_user_identity', options_list=['--entra-user-identity','-e'], arg_group='HTTP', nargs='+',
help='One or more Entra user identities (object ID or user principal name) to associate with the cluster. Multiple values can be separated by spaces or commas.')
c.argument('entra_user_full_info', options_list=['--entra-user-full-info','-E'], arg_group='HTTP', completer=FilesCompleter(), type=shell_safe_json_parse,
help='The Entra user information to associate with the cluster. '
'This can be provided as a JSON string or from a file using the `@{path}` syntax. '
'Each entry should include "objectID", "upn", and "displayName" fields. '
'Please see: https://github.com/Azure/azure-cli/blob/dev/src/azure-cli/azure/cli/command_modules/hdinsight/tests/latest/entrauserconfig.json')

# SSH
c.argument('ssh_username', options_list=['--ssh-user', '-U'], arg_group='SSH',
Expand Down Expand Up @@ -411,3 +418,17 @@ def load_arguments(self, _):
with self.argument_context('hdinsight autoscale condition delete') as c:
c.argument('index', nargs='+', type=int,
help='The Space-separated list of condition indices which starts with 0 to delete.')

# credentials
with self.argument_context('hdinsight credentials update') as c:
c.argument('http_username', options_list=['--http-user', '-u'], arg_group='HTTP',
help='HTTP username for the cluster. Default: admin.')
c.argument('http_password', options_list=['--http-password', '-p'], arg_group='HTTP',
help='HTTP password for the cluster. Will prompt if not given.')
c.argument('entra_user_identity', options_list=['--entra-user-identity','-e'], arg_group='HTTP', nargs='+',
help='One or more Entra user identities (object ID or user principal name) to associate with the cluster. Multiple values can be separated by spaces or commas.')
c.argument('entra_user_full_info', options_list=['--entra-user-full-info','-E'], arg_group='HTTP', completer=FilesCompleter(), type=shell_safe_json_parse,
help='The Entra user information to associate with the cluster. '
'This can be provided as a JSON string or from a file using the `@{path}` syntax. '
'Each entry should include "objectID", "upn", and "displayName" fields. '
'Please see: https://github.com/Azure/azure-cli/blob/dev/src/azure-cli/azure/cli/command_modules/hdinsight/tests/latest/entrauserconfig.json')
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,8 @@ def load_command_table(self, _): # pylint: disable=too-many-statements
g.custom_command('list', 'list_autoscale_condition')
g.custom_command('delete', 'delete_autoscale_condition', supports_no_wait=True, confirmation=True)
g.wait_command('wait')

# credentials operations
with self.command_group('hdinsight credentials', hdinsight_clusters_sdk, client_factory=cf_hdinsight_clusters) as g:
g.show_command('show', 'get_gateway_settings')
g.custom_command('update', 'update_gateway_settings', supports_no_wait=True, confirmation=True)
62 changes: 50 additions & 12 deletions src/azure-cli/azure/cli/command_modules/hdinsight/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from knack.prompting import prompt_pass, NoTTYException
from knack.util import CLIError
from azure.cli.core.util import sdk_no_wait, user_confirmation
from azure.cli.core.azclierror import RequiredArgumentMissingError
from azure.cli.core.azclierror import RequiredArgumentMissingError, MutuallyExclusiveArgumentError

logger = get_logger(__name__)

Expand All @@ -20,7 +20,7 @@ def create_cluster(cmd, client, cluster_name, resource_group_name, cluster_type,
kafka_client_group_id=None, kafka_client_group_name=None,
workernode_count=3, workernode_data_disks_per_node=None,
workernode_data_disk_storage_account_type=None, workernode_data_disk_size=None,
http_username=None, http_password=None,
http_username=None, http_password=None, entra_user_identity=None, entra_user_full_info=None,
ssh_username='sshuser', ssh_password=None, ssh_public_key=None,
storage_account=None, storage_account_key=None,
storage_default_container=None, storage_default_filesystem=None,
Expand All @@ -41,7 +41,7 @@ def create_cluster(cmd, client, cluster_name, resource_group_name, cluster_type,
enable_compute_isolation=None, host_sku=None, zones=None, private_link_configurations=None,
no_validation_timeout=False, outbound_dependencies_managed_type=None):
from .util import build_identities_info, build_virtual_network_profile, parse_domain_name, \
get_storage_account_endpoint, validate_esp_cluster_create_params, set_vm_size
get_storage_account_endpoint, validate_esp_cluster_create_params, set_vm_size, is_wasb_storage_account, get_entra_user_info
from azure.mgmt.hdinsight.models import ClusterCreateParametersExtended, ClusterCreateProperties, OSType, \
ClusterDefinition, ComputeProfile, HardwareProfile, Role, OsProfile, LinuxOperatingSystemProfile, \
StorageProfile, StorageAccount, DataDisksGroups, SecurityProfile, \
Expand Down Expand Up @@ -84,17 +84,23 @@ def create_cluster(cmd, client, cluster_name, resource_group_name, cluster_type,
if not http_username:
http_username = 'admin' # Implement default logic here, in case a user specifies the username in configurations

if not http_password:
if not http_password and not entra_user_identity and not entra_user_full_info:
try:
http_password = prompt_pass('HTTP password for the cluster:', confirm=True)
except NoTTYException:
raise CLIError('Please specify --http-password in non-interactive mode.')

# Update the cluster config with the HTTP credentials
gateway_config['restAuthCredential.isEnabled'] = 'true' # HTTP credentials are required
http_username = http_username or gateway_config['restAuthCredential.username']
gateway_config['restAuthCredential.username'] = http_username
gateway_config['restAuthCredential.password'] = http_password
if not entra_user_identity and not entra_user_full_info:
gateway_config['restAuthCredential.isEnabled'] = 'true' # HTTP credentials are required
http_username = http_username or gateway_config['restAuthCredential.username']
gateway_config['restAuthCredential.username'] = http_username
gateway_config['restAuthCredential.password'] = http_password
else:
if entra_user_identity and entra_user_full_info:
raise MutuallyExclusiveArgumentError('Cannot provide both --entra-user-identity and --entra-user-full-info parameters.')
gateway_config['restAuthCredential.isEnabled'] = 'false'
gateway_config['restAuthEntraUsers'] = get_entra_user_info(cmd, entra_user_identity, entra_user_full_info)
cluster_configurations['gateway'] = gateway_config

# Validate whether SSH credentials were provided
Expand All @@ -107,13 +113,19 @@ def create_cluster(cmd, client, cluster_name, resource_group_name, cluster_type,
raise CLIError('Either the default container or the default filesystem can be specified, but not both.')

# Retrieve primary blob service endpoint
is_wasb = not storage_account_managed_identity
is_wasb = None
if storage_default_container:
is_wasb = True
elif storage_default_filesystem:
is_wasb = False
else:
is_wasb = is_wasb_storage_account(cmd, storage_account)
storage_account_endpoint = None
if storage_account:
storage_account_endpoint = get_storage_account_endpoint(cmd, storage_account, is_wasb)

# Attempt to infer the storage account key from the endpoint
if not storage_account_key and storage_account and is_wasb:
if not storage_account_key and storage_account and not storage_account_managed_identity and is_wasb:
from .util import get_key_for_storage_account
logger.info('Storage account key not specified. Attempting to retrieve key...')
key = get_key_for_storage_account(cmd, storage_account)
Expand All @@ -130,8 +142,8 @@ def create_cluster(cmd, client, cluster_name, resource_group_name, cluster_type,
logger.warning('Default ADLS file system not specified, using "%s".', storage_default_filesystem)

# Validate storage info parameters
if is_wasb and not _all_or_none(storage_account, storage_account_key, storage_default_container):
raise CLIError('If storage details are specified, the storage account, storage account key, '
if is_wasb and not _all_or_none(storage_account, storage_default_container):
raise CLIError('If storage details are specified, the storage account, '
'and the default container must be specified.')
if not is_wasb and not _all_or_none(storage_account, storage_default_filesystem):
raise CLIError('If storage details are specified, the storage account, '
Expand Down Expand Up @@ -903,3 +915,29 @@ def _extract_and_validate_autoscale_configuration(cluster):
def _validate_schedule_configuration(autoscale_configuration):
if not autoscale_configuration.recurrence:
raise CLIError('The cluster has not enabled Schedule-based autoscale.')

def update_gateway_settings(cmd, client, cluster_name, resource_group_name, http_username=None, http_password=None, entra_user_identity=None, entra_user_full_info=None, no_wait=False):
from azure.mgmt.hdinsight.models import UpdateGatewaySettingsParameters
from .util import get_entra_user_info
if not http_password and not entra_user_identity and not entra_user_full_info:
try:
http_password = prompt_pass('HTTP password for the cluster:', confirm=True)
except NoTTYException:
raise CLIError('Please specify --http-password in non-interactive mode.')
if http_password and not http_username:
http_username = 'admin'
if entra_user_identity and entra_user_full_info:
raise MutuallyExclusiveArgumentError('Cannot provide both --entra-user-identity and --entra-user-full-info parameters.')
rest_auth_entra_users_data = None
if entra_user_identity or entra_user_full_info:
rest_auth_entra_users_data = get_entra_user_info(cmd, entra_user_identity, entra_user_full_info, False)
update_gateway_settings_parameters = UpdateGatewaySettingsParameters(
is_credential_enabled = bool(http_password),
user_name = http_username,
password = http_password,
rest_auth_entra_users = rest_auth_entra_users_data
)
try:
return sdk_no_wait(no_wait, client.begin_update_gateway_settings, resource_group_name, cluster_name, update_gateway_settings_parameters)
except Exception as ex:
raise CLIError(str(ex))
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[
{
"objectID": "00000000-0000-0000-0000-000000000000",
"displayName": "displayName",
"upn": "test@example.com"
},
{
"objectID": "00000000-0000-0000-0000-000000000000",
"displayName": "displayName",
"upn": "test@example.com"
}
]
Loading
Loading