Skip to content

Commit 2cf7b76

Browse files
[CosmosDB] Fix #32608: az cosmosdb restore: Fix "Database Account does not exist" error during polling (#32752)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 141ec11 commit 2cf7b76

File tree

3 files changed

+212
-5
lines changed

3 files changed

+212
-5
lines changed

src/azure-cli/azure/cli/command_modules/cosmosdb/custom.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,11 @@ def _create_database_account(client,
273273
locations = []
274274
locations.append(Location(location_name=arm_location, failover_priority=0, is_zone_redundant=False))
275275

276+
for loc in locations:
277+
if loc.failover_priority == 0:
278+
arm_location = loc.location_name
279+
break
280+
276281
managed_service_identity = None
277282
SYSTEM_ID = '[system]'
278283
enable_system = False
@@ -409,8 +414,22 @@ def _create_database_account(client,
409414
)
410415

411416
async_docdb_create = client.begin_create_or_update(resource_group_name, account_name, params)
412-
docdb_account = async_docdb_create.result()
413-
docdb_account = client.get(resource_group_name, account_name) # Workaround
417+
try:
418+
docdb_account = async_docdb_create.result()
419+
except HttpResponseError as ex:
420+
message = str(ex)
421+
if (is_restore_request and
422+
ex.status_code == 403 and
423+
"does not exist" in message and
424+
("Database Account" in message or "Forbidden" in message)):
425+
logger.warning(
426+
"Encountered known service issue (403 'does not exist') while restoring Cosmos DB account '%s' "
427+
"in resource group '%s'. Using client.get() as a workaround. Raw error: %s",
428+
account_name, resource_group_name, ex
429+
)
430+
docdb_account = client.get(resource_group_name, account_name)
431+
else:
432+
raise ex
414433
return docdb_account
415434

416435

@@ -3518,6 +3537,24 @@ def cli_offline_region(client,
35183537
resource_group_name,
35193538
region):
35203539

3540+
# Function to normalize region name
3541+
def _normalize_region(region_name):
3542+
return region_name.replace(' ', '').lower()
3543+
3544+
# Get the account to check for the region name
3545+
account = client.get(resource_group_name, account_name)
3546+
input_region_normalized = _normalize_region(region)
3547+
matched_region = None
3548+
3549+
# Check matches in both read and write locations
3550+
for loc in account.locations:
3551+
if _normalize_region(loc.location_name) == input_region_normalized:
3552+
matched_region = loc.location_name
3553+
break
3554+
3555+
if matched_region:
3556+
region = matched_region
3557+
35213558
region_parameter_for_offline = RegionForOnlineOffline(region=region)
35223559
return client.begin_offline_region(
35233560
resource_group_name=resource_group_name,

src/azure-cli/azure/cli/command_modules/cosmosdb/tests/latest/test_cosmosdb_backuprestore_scenario.py

Lines changed: 165 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# --------------------------------------------------------------------------------------------
55

66
import os
7+
import sys
78
import unittest
89
from unittest import mock
910

@@ -518,4 +519,167 @@ def test_cosmosdb_xrr_single_region_account(self, resource_group):
518519
assert restored_account['restoreParameters']['restoreSource'] == restorable_database_account['id']
519520
assert restored_account['restoreParameters']['restoreTimestampInUtc'] == restore_ts_string
520521
assert restored_account['restoreParameters']['sourceBackupLocation'] == source_loc_for_xrr
521-
assert restored_account['writeLocations'][0]['locationName'] == 'North Central US'
522+
assert restored_account['writeLocations'][0]['locationName'] == 'North Central US'
523+
524+
525+
class CosmosDBRestoreUnitTests(unittest.TestCase):
526+
def setUp(self):
527+
# Mock dependencies that might be missing or problematic to import
528+
if 'azure.mgmt.cosmosdb.models' not in sys.modules:
529+
sys.modules['azure.mgmt.cosmosdb.models'] = mock.MagicMock()
530+
if 'azure.cli.core.util' not in sys.modules:
531+
sys.modules['azure.cli.core.util'] = mock.MagicMock()
532+
if 'knack.log' not in sys.modules:
533+
sys.modules['knack.log'] = mock.MagicMock()
534+
# Mocking knack.util.CLIError is crucial if it's used in custom.py
535+
if 'knack.util' not in sys.modules:
536+
mock_knack_util = mock.MagicMock()
537+
mock_knack_util.CLIError = Exception
538+
sys.modules['knack.util'] = mock_knack_util
539+
540+
# Ensure Azure Core Exceptions are available
541+
try:
542+
import azure.core.exceptions
543+
except ImportError:
544+
mock_core_exceptions = mock.MagicMock()
545+
# Define minimal exception class
546+
class HttpResponseError(Exception):
547+
def __init__(self, message=None, response=None, **kwargs):
548+
self.message = message
549+
self.response = response
550+
self.status_code = kwargs.get('status_code', None)
551+
def __str__(self):
552+
return self.message or ""
553+
mock_core_exceptions.HttpResponseError = HttpResponseError
554+
mock_core_exceptions.ResourceNotFoundError = Exception
555+
sys.modules['azure.core.exceptions'] = mock_core_exceptions
556+
557+
def test_restore_handles_forbidden_error(self):
558+
from azure.core.exceptions import HttpResponseError
559+
# Lazy import to ensure mocks are applied first
560+
from azure.cli.command_modules.cosmosdb.custom import _create_database_account
561+
562+
# Setup mocks
563+
client = mock.MagicMock()
564+
565+
# Simulate the LRO poller raising the specific error
566+
poller = mock.MagicMock()
567+
error_json = '{"code":"Forbidden","message":"Database Account riks-models-003-acc-westeurope does not exist"}'
568+
exception = HttpResponseError(message=error_json)
569+
exception.status_code = 403
570+
571+
# side_effect raises the exception when called
572+
poller.result.side_effect = exception
573+
client.begin_create_or_update.return_value = poller
574+
575+
# Simulate client.get returning the account successfully
576+
mock_account = mock.MagicMock()
577+
mock_account.provisioning_state = "Succeeded"
578+
client.get.return_value = mock_account
579+
580+
# Parameters
581+
resource_group_name = "rg"
582+
account_name = "myaccount"
583+
584+
# Call the private function directly to verify logic
585+
result = _create_database_account(
586+
client=client,
587+
resource_group_name=resource_group_name,
588+
account_name=account_name,
589+
locations=[],
590+
is_restore_request=True,
591+
arm_location="westeurope",
592+
restore_source="/subscriptions/sub/providers/Microsoft.DocumentDB/locations/westeurope/restorableDatabaseAccounts/source-id",
593+
restore_timestamp="2026-01-01T00:00:00+00:00"
594+
)
595+
596+
# Assertions
597+
# 1. begin_create_or_update called
598+
client.begin_create_or_update.assert_called()
599+
# 2. poller.result() called (and raised exception)
600+
poller.result.assert_called()
601+
# 3. client.get called (recovery mechanism)
602+
client.get.assert_called_with(resource_group_name, account_name)
603+
# 4. Result is the account returned by get
604+
self.assertEqual(result, mock_account)
605+
606+
def test_restore_raises_other_errors(self):
607+
from azure.core.exceptions import HttpResponseError
608+
from azure.cli.command_modules.cosmosdb.custom import _create_database_account
609+
610+
# Setup mocks
611+
client = mock.MagicMock()
612+
poller = mock.MagicMock()
613+
614+
# Different error
615+
exception = HttpResponseError(message="Some other error")
616+
exception.status_code = 500
617+
poller.result.side_effect = exception
618+
client.begin_create_or_update.return_value = poller
619+
620+
with self.assertRaises(HttpResponseError):
621+
_create_database_account(
622+
client=client,
623+
resource_group_name="rg",
624+
account_name="myaccount",
625+
is_restore_request=True,
626+
arm_location="westeurope",
627+
restore_source="src",
628+
restore_timestamp="ts"
629+
)
630+
631+
def test_normal_create_does_not_suppress_error(self):
632+
from azure.core.exceptions import HttpResponseError
633+
from azure.cli.command_modules.cosmosdb.custom import _create_database_account
634+
635+
# Setup mocks
636+
client = mock.MagicMock()
637+
poller = mock.MagicMock()
638+
639+
# Same error but NOT a restore request
640+
error_json = '{"code":"Forbidden","message":"Database Account riks-models-003-acc-westeurope does not exist"}'
641+
exception = HttpResponseError(message=error_json)
642+
exception.status_code = 403
643+
poller.result.side_effect = exception
644+
client.begin_create_or_update.return_value = poller
645+
646+
with self.assertRaises(HttpResponseError):
647+
_create_database_account(
648+
client=client,
649+
resource_group_name="rg",
650+
account_name="myaccount",
651+
is_restore_request=False, # Normal create
652+
arm_location="westeurope"
653+
)
654+
655+
def test_normal_create_success(self):
656+
from azure.cli.command_modules.cosmosdb.custom import _create_database_account
657+
658+
# Setup mocks
659+
client = mock.MagicMock()
660+
poller = mock.MagicMock()
661+
662+
# Simulate successful creation
663+
mock_created_account = mock.MagicMock()
664+
mock_created_account.provisioning_state = "Succeeded"
665+
poller.result.return_value = mock_created_account
666+
client.begin_create_or_update.return_value = poller
667+
668+
# Call the private function
669+
result = _create_database_account(
670+
client=client,
671+
resource_group_name="rg",
672+
account_name="myaccount",
673+
is_restore_request=False,
674+
arm_location="westeurope"
675+
)
676+
677+
# Assertions
678+
# 1. begin_create_or_update called
679+
client.begin_create_or_update.assert_called()
680+
# 2. poller.result() called
681+
poller.result.assert_called()
682+
# 3. client.get should NOT be called since result() succeeded
683+
client.get.assert_not_called()
684+
# 4. Result matches
685+
self.assertEqual(result, mock_created_account)

src/azure-cli/azure/cli/command_modules/cosmosdb/tests/latest/test_cosmosdb_commands.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,13 @@ def test_locations_database_accounts(self, resource_group):
239239
assert account1['readLocations'][0]['failoverPriority'] == 1 or account1['readLocations'][1]['failoverPriority'] == 1
240240

241241
self.cmd('az cosmosdb failover-priority-change -n {acc} -g {rg} --failover-policies {read_location}=0 {write_location}=1')
242-
account2 = self.cmd('az cosmosdb show -n {acc} -g {rg}').get_output_in_json()
242+
import time
243+
for _ in range(0, 10):
244+
account2 = self.cmd('az cosmosdb show -n {acc} -g {rg}').get_output_in_json()
245+
if account2['writeLocations'][0]['locationName'] == "West US":
246+
break
247+
time.sleep(5)
248+
243249
assert len(account2['writeLocations']) == 1
244250
assert len(account2['readLocations']) == 2
245251

@@ -260,7 +266,7 @@ def test_locations_database_accounts_offline(self, resource_group):
260266
'read_location': read_location
261267
})
262268

263-
account_pre_offline = self.cmd('az cosmosdb create -n {acc} -g {rg} --locations regionName={write_location} failoverPriority=0 --locations regionName={read_location} failoverPriority=1').get_output_in_json()
269+
account_pre_offline = self.cmd('az cosmosdb create -n {acc} -g {rg} --enable-automatic-failover --locations regionName={write_location} failoverPriority=0 --locations regionName={read_location} failoverPriority=1').get_output_in_json()
264270

265271
assert account_pre_offline['writeLocations'][0]['locationName'] == "East US"
266272

0 commit comments

Comments
 (0)