Skip to content

Commit 97d3abc

Browse files
Add clean up activities when migrate-credentials cmd fails intermittently (#2479)
<!-- REMOVE IRRELEVANT COMMENTS BEFORE CREATING A PULL REQUEST --> ## Changes This PR adds functionality to delete access connectors and storage credentials created during the migrate-credentials cmd for azure and when it fails intermittently. <!-- Summary of your changes that are easy to understand. Add screenshots when necessary --> ### Linked issues <!-- DOC: Link issue with a keyword: close, closes, closed, fix, fixes, fixed, resolve, resolves, resolved. See https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword --> Resolves #2362 ### Functionality - [ ] added relevant user documentation - [ ] added new CLI command - [ ] modified existing command: `databricks labs ucx ...` - [ ] added a new workflow - [ ] modified existing workflow: `...` - [ ] added a new table - [ ] modified existing table: `...` ### Tests <!-- How is this tested? Please see the checklist below and also describe any other relevant tests --> - [ ] manually tested - [ ] added unit tests - [ ] added integration tests - [ ] verified on staging environment (screenshot attached) --------- Co-authored-by: Cor <[email protected]>
1 parent fc554ae commit 97d3abc

File tree

6 files changed

+191
-43
lines changed

6 files changed

+191
-43
lines changed

src/databricks/labs/ucx/azure/access.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,9 +512,23 @@ def create_access_connectors_for_storage_accounts(self) -> list[tuple[AccessConn
512512
thread_name = "Creating access connectors for storage accounts"
513513
results, errors = Threads.gather(thread_name, tasks)
514514
if len(errors) > 0:
515+
logger.error(
516+
"Error creating access connectors. Please review the error message and resolve the issue before trying again. "
517+
"Removing successfully created access connectors."
518+
)
519+
delete_access_connectors = [access_connector for access_connector, _ in results]
520+
self.delete_access_connectors(*delete_access_connectors)
515521
raise ManyError(errors)
516522
return list(results)
517523

524+
def delete_storage_credential(self, *storage_credentials: Any):
525+
for storage_credential in storage_credentials:
526+
self._ws.storage_credentials.delete(storage_credential.name)
527+
528+
def delete_access_connectors(self, *access_connectors: Any):
529+
for access_connector in access_connectors:
530+
self._azurerm.delete_access_connector(str(access_connector.id))
531+
518532
def _apply_storage_permission(
519533
self,
520534
principal_id: str,

src/databricks/labs/ucx/azure/credentials.py

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from databricks.labs.blueprint.tui import Prompts
66
from databricks.sdk import WorkspaceClient
77
from databricks.sdk.errors import BadRequest
8-
from databricks.sdk.errors.platform import InvalidParameterValue
8+
from databricks.sdk.errors.platform import InvalidParameterValue, PermissionDenied, NotFound
99
from databricks.sdk.service.catalog import (
1010
AzureManagedIdentityRequest,
1111
AzureServicePrincipal,
@@ -264,19 +264,29 @@ def _migrate_service_principals(
264264
f"Service principal '{spn.permission_mapping.principal}' accesses storage account "
265265
f"'{spn.permission_mapping.prefix}' with non-Allow network configuration"
266266
)
267+
try:
267268

268-
storage_credential_info = self._storage_credential_manager.create_with_client_secret(spn)
269-
validation_results = self._storage_credential_manager.validate(
270-
storage_credential_info,
271-
spn.permission_mapping.prefix,
272-
)
273-
execution_result.append(validation_results)
269+
storage_credential_info = self._storage_credential_manager.create_with_client_secret(spn)
270+
validation_results = self._storage_credential_manager.validate(
271+
storage_credential_info,
272+
spn.permission_mapping.prefix,
273+
)
274+
execution_result.append(validation_results)
275+
except (PermissionDenied, NotFound, BadRequest) as e:
276+
logger.error(
277+
"Error creating storage credentials. Please check error message and try again."
278+
"Rolling back storage credential that are created."
279+
)
280+
self._resource_permissions.delete_storage_credential(execution_result)
281+
raise e
274282
return execution_result
275283

276-
def _create_storage_credentials_for_storage_accounts(self) -> list[StorageCredentialValidationResult]:
284+
def _create_storage_credentials_for_storage_accounts(
285+
self,
286+
) -> list[StorageCredentialValidationResult]:
277287
access_connectors = self._resource_permissions.create_access_connectors_for_storage_accounts()
278288

279-
execution_results = []
289+
execution_results: list[StorageCredentialValidationResult] = []
280290
for access_connector, url in access_connectors:
281291
storage_credential_info = self._ws.storage_credentials.create(
282292
access_connector.name,
@@ -288,6 +298,15 @@ def _create_storage_credentials_for_storage_accounts(self) -> list[StorageCreden
288298
validation_results = self._storage_credential_manager.validate(storage_credential_info, url)
289299
except BadRequest:
290300
logger.warning(f"Could not validate storage credential {storage_credential_info.name} for url {url}")
301+
except (PermissionDenied, NotFound):
302+
logger.error(
303+
"Error creating access connectors. Please review the error message and resolve the issue before "
304+
"trying again. "
305+
"Removing successfully created storage credentials."
306+
)
307+
self._resource_permissions.delete_storage_credential(*execution_results)
308+
delete_access_connectors = [access_connector for access_connector, _ in access_connectors]
309+
self._resource_permissions.delete_access_connectors(*delete_access_connectors)
291310
else:
292311
execution_results.append(validation_results)
293312

@@ -298,7 +317,7 @@ def run(self, prompts: Prompts, include_names: set[str] | None = None) -> list[S
298317
"[RECOMMENDED] Please confirm to create an access connector with a managed identity for each storage "
299318
"account."
300319
)
301-
ac_results = []
320+
ac_results: list[StorageCredentialValidationResult] = []
302321
if plan_confirmed:
303322
ac_results = self._create_storage_credentials_for_storage_accounts()
304323

src/databricks/labs/ucx/azure/resources.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -761,11 +761,10 @@ def create_or_update_access_connector(
761761

762762
return access_connector
763763

764-
def delete_access_connector(self, subscription_id: str, resource_group_name: str, name: str) -> None:
764+
def delete_access_connector(self, url: str) -> None:
765765
"""Delete an access connector.
766766
767767
Docs:
768768
https://learn.microsoft.com/en-us/rest/api/databricks/access-connectors/delete?view=rest-databricks-2023-05-01&tabs=HTTP
769769
"""
770-
url = f"/subscriptions/{subscription_id}/resourceGroups/{resource_group_name}/providers/Microsoft.Databricks/accessConnectors/{name}"
771770
self._mgmt.delete(url, api_version="2023-05-01")

tests/unit/azure/test_access.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
GetWorkspaceWarehouseConfigResponseSecurityPolicy,
1616
SetWorkspaceWarehouseConfigRequestSecurityPolicy,
1717
)
18+
from databricks.labs.blueprint.parallel import ManyError
1819
from databricks.sdk.service.workspace import GetSecretResponse
1920

2021
from databricks.labs.ucx.azure.access import AzureResourcePermissions
@@ -1064,3 +1065,62 @@ def test_create_access_connectors_for_storage_accounts_log_permission_applied(ca
10641065
w.cluster_policies.get.assert_not_called()
10651066
w.secrets.get_secret.assert_not_called()
10661067
w.secrets.create_scope.assert_not_called()
1068+
1069+
1070+
def test_create_access_connectors_for_storage_accounts_rollback(caplog):
1071+
"""Log that the permissions for the access connector are applied."""
1072+
w = create_autospec(WorkspaceClient)
1073+
1074+
rows = {
1075+
"SELECT \\* FROM `hive_metastore`.`ucx`.`external_locations`": [
1076+
["abfss://[email protected]/folder1", "1"],
1077+
["abfss://[email protected]/folder2", "1"],
1078+
]
1079+
}
1080+
backend = MockBackend(rows=rows)
1081+
1082+
location = ExternalLocations(w, backend, "ucx")
1083+
installation = MockInstallation()
1084+
1085+
azure_resources = create_autospec(AzureResources)
1086+
azure_resources.storage_accounts.return_value = [
1087+
StorageAccount(
1088+
id=AzureResource('/subscriptions/abc/providers/Microsoft.Storage/storageAccounts/storage1'),
1089+
name="storage1",
1090+
location="westeu",
1091+
default_network_action="Allow",
1092+
),
1093+
StorageAccount(
1094+
id=AzureResource('/subscriptions/abc/providers/Microsoft.Storage/storageAccounts/storage2'),
1095+
name="storage2",
1096+
location="westeu",
1097+
default_network_action="Allow",
1098+
),
1099+
]
1100+
1101+
access_connector_id = AzureResource(
1102+
"/subscriptions/test/resourceGroups/rg-test/providers/Microsoft.Databricks/accessConnectors/ac-test"
1103+
)
1104+
azure_resources.create_or_update_access_connector.side_effect = [
1105+
AccessConnector(
1106+
id=access_connector_id,
1107+
name="ac-test",
1108+
location="westeu",
1109+
provisioning_state="Succeeded",
1110+
identity_type="SystemAssigned",
1111+
principal_id="test",
1112+
tenant_id="test",
1113+
),
1114+
PermissionDenied(),
1115+
]
1116+
1117+
azure_resource_permission = AzureResourcePermissions(installation, w, azure_resources, location)
1118+
1119+
with pytest.raises(ManyError):
1120+
azure_resource_permission.create_access_connectors_for_storage_accounts()
1121+
assert any("STORAGE_BLOB_DATA_CONTRIBUTOR" in message for message in caplog.messages)
1122+
1123+
azure_resources.delete_access_connector.assert_called_with(
1124+
'/subscriptions/test/resourceGroups/rg-test/providers/Microsoft.Databricks/accessConnectors/ac-test'
1125+
)
1126+
w.secrets.create_scope.assert_not_called()

0 commit comments

Comments
 (0)