Skip to content

Commit 12b0514

Browse files
bvliucopybara-github
authored andcommitted
Add support for flexible server PremiumSSD v2.
PiperOrigin-RevId: 853313032
1 parent 9eeff99 commit 12b0514

File tree

2 files changed

+250
-4
lines changed

2 files changed

+250
-4
lines changed

perfkitbenchmarker/providers/azure/azure_flexible_server.py

Lines changed: 97 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import datetime
2323
import json
2424
import logging
25+
import re
2526
import time
2627
from typing import Any, Tuple
2728

@@ -57,6 +58,10 @@
5758
CREATE_AZURE_DB_TIMEOUT = 60 * 120
5859

5960

61+
class AzureAsyncOperationInProgressError(Exception):
62+
"""Exception raised when an Azure async operation is still in progress."""
63+
64+
6065
class AzureFlexibleServer(azure_relational_db.AzureRelationalDb):
6166
"""An object representing an Azure Flexible server.
6267
@@ -91,10 +96,6 @@ def __init__(self, relational_db_spec: Any):
9196
self.storage_type = (
9297
self.spec.db_disk_spec.disk_type or azure_disk.PREMIUM_STORAGE
9398
)
94-
if self.storage_type == azure_disk.PREMIUM_STORAGE_V2:
95-
raise errors.Config.InvalidValue(
96-
'Premium Storage v2 is not supported for Postgres Flexible Server.'
97-
)
9899

99100
@staticmethod
100101
def GetDefaultEngineVersion(engine: str) -> str:
@@ -151,6 +152,13 @@ def _Create(self) -> None:
151152
self.spec.engine_version,
152153
'--yes',
153154
]
155+
if (
156+
self.spec.engine == sql_engine_utils.FLEXIBLE_SERVER_POSTGRES
157+
and self.storage_type == azure_disk.PREMIUM_STORAGE_V2
158+
):
159+
# Postgres PremiumV2_LRS requires creation through REST API.
160+
self._CreatePremiumSsdV2()
161+
return
154162
if self.storage_type:
155163
cmd.extend([
156164
'--storage-type',
@@ -177,6 +185,91 @@ def _Create(self) -> None:
177185

178186
vm_util.IssueCommand(cmd, timeout=CREATE_AZURE_DB_TIMEOUT)
179187

188+
@vm_util.Retry(
189+
timeout=CREATE_AZURE_DB_TIMEOUT,
190+
retryable_exceptions=(AzureAsyncOperationInProgressError,),
191+
)
192+
def _WaitForAzureAsyncOperation(self, async_url: str) -> None:
193+
"""Polls Azure async operation URL until completion."""
194+
cmd = [azure.AZURE_PATH, 'rest', '--method', 'GET', '--uri', async_url]
195+
stdout, _, _ = vm_util.IssueCommand(cmd, raise_on_failure=True)
196+
status_info = json.loads(stdout)
197+
status = status_info.get('status')
198+
if not status and 'properties' in status_info:
199+
status = status_info['properties'].get('status')
200+
201+
if status == 'Succeeded':
202+
return
203+
if status == 'Failed':
204+
raise errors.Resource.CreationError(
205+
f'Azure async operation failed: {stdout}'
206+
)
207+
raise AzureAsyncOperationInProgressError(
208+
f'Azure async operation status unknown: {status_info}'
209+
)
210+
211+
def _CreatePremiumSsdV2(self) -> None:
212+
"""Creates a Postgres PremiumV2_LRS instance."""
213+
subscription_id = util.GetSubscriptionId()
214+
uri = (
215+
f'https://management.azure.com/subscriptions/{subscription_id}'
216+
f'/resourceGroups/{self.resource_group.name}'
217+
'/providers/Microsoft.DBforPostgreSQL/flexibleServers/'
218+
f'{self.instance_id}?api-version=2024-08-01'
219+
)
220+
body = {
221+
'location': self.region,
222+
'sku': {
223+
'name': self.spec.db_spec.machine_type,
224+
'tier': self.spec.db_tier,
225+
},
226+
'properties': {
227+
'administratorLogin': self.spec.database_username,
228+
'administratorLoginPassword': self.spec.database_password,
229+
'version': self.spec.engine_version,
230+
'storage': {
231+
'storageSizeGB': self.spec.db_disk_spec.disk_size,
232+
'type': azure_disk.PREMIUM_STORAGE_V2,
233+
},
234+
'backup': {
235+
'backupRetentionDays': 7,
236+
'geoRedundantBackup': 'Disabled',
237+
},
238+
'network': {'publicNetworkAccess': 'Enabled'},
239+
'highAvailability': {'mode': 'ZoneRedundant'},
240+
},
241+
}
242+
if self.spec.db_disk_spec.provisioned_iops:
243+
body['properties']['storage'][
244+
'iops'
245+
] = self.spec.db_disk_spec.provisioned_iops
246+
if self.spec.db_disk_spec.provisioned_throughput:
247+
body['properties']['storage'][
248+
'throughput'
249+
] = self.spec.db_disk_spec.provisioned_throughput
250+
cmd = [
251+
azure.AZURE_PATH,
252+
'rest',
253+
'--method',
254+
'PUT',
255+
'--uri',
256+
uri,
257+
'--body',
258+
json.dumps(body),
259+
'--verbose',
260+
]
261+
_, stderr, _ = vm_util.IssueCommand(
262+
cmd, timeout=CREATE_AZURE_DB_TIMEOUT, raise_on_failure=False
263+
)
264+
match = re.search(r"'Azure-AsyncOperation': '([^']*)'", stderr)
265+
if not match:
266+
raise errors.Resource.CreationError(
267+
'Could not find Azure-AsyncOperation header in az rest verbose'
268+
' output.'
269+
)
270+
async_operation_url = match.group(1)
271+
self._WaitForAzureAsyncOperation(async_operation_url)
272+
180273
def GetAzCommandForEngine(self) -> str:
181274
engine = self.spec.engine
182275
if engine == sql_engine_utils.FLEXIBLE_SERVER_POSTGRES:

tests/providers/azure/azure_flexible_server_test.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import datetime
22
import inspect
33
import json
4+
import time
45
import unittest
56

67
from absl import flags
@@ -231,5 +232,157 @@ def testConstructMysqlWithThroughputRaises(self):
231232
bm_spec.ConstructRelationalDb()
232233

233234

235+
class AzureFlexibleServerPremiumV2CreateTestCase(
236+
pkb_common_test_case.PkbCommonTestCase
237+
):
238+
239+
def setUp(self):
240+
super().setUp()
241+
FLAGS.cloud = provider_info.AZURE
242+
FLAGS.run_uri = 'test_run_uri'
243+
self.resource_group_patch = self.enter_context(
244+
mock.patch.object(
245+
azure_network, 'GetResourceGroup', return_value=mock.Mock()
246+
)
247+
)
248+
self.enter_context(
249+
mock.patch.object(util, 'GetSubscriptionId', return_value='test-sub')
250+
)
251+
self.enter_context(mock.patch.object(time, 'sleep'))
252+
253+
def testCreatePostgresPremiumV2(self):
254+
mock_cmd = self.MockIssueCommand({
255+
'rest --method PUT': [(
256+
'',
257+
"'Azure-AsyncOperation': 'https://management.azure.com/async_op'",
258+
0,
259+
)],
260+
'rest --method GET': [
261+
('{"status": "InProgress"}', '', 0),
262+
('{"status": "Succeeded"}', '', 0),
263+
],
264+
})
265+
yaml_spec = inspect.cleandoc(f"""
266+
sysbench:
267+
relational_db:
268+
cloud: {provider_info.AZURE}
269+
engine: {sql_engine_utils.FLEXIBLE_SERVER_POSTGRES}
270+
engine_version: '13'
271+
db_tier: GeneralPurpose
272+
db_spec:
273+
{provider_info.AZURE}:
274+
machine_type: Standard_D2ds_v4
275+
zone: eastus
276+
db_disk_spec:
277+
{provider_info.AZURE}:
278+
disk_size: 128
279+
disk_type: PremiumV2_LRS
280+
provisioned_iops: 1000
281+
provisioned_throughput: 200
282+
vm_groups:
283+
clients:
284+
vm_spec: *default_dual_core
285+
disk_spec: *default_500_gb
286+
""")
287+
bm_spec = pkb_common_test_case.CreateBenchmarkSpecFromYaml(
288+
yaml_spec, 'sysbench'
289+
)
290+
bm_spec.ConstructRelationalDb()
291+
292+
bm_spec.relational_db._Create()
293+
294+
self.assertEqual(mock_cmd.func_to_mock.call_count, 3)
295+
put_call, get_call1, get_call2 = mock_cmd.func_to_mock.call_args_list
296+
297+
# Check az rest PUT call
298+
put_cmd = ' '.join(put_call[0][0])
299+
with self.subTest(name='RestApi'):
300+
self.assertIn('rest --method PUT', put_cmd)
301+
self.assertIn('PremiumV2_LRS', put_cmd)
302+
self.assertIn('"iops": 1000', put_cmd)
303+
self.assertIn('"throughput": 200', put_cmd)
304+
305+
# Check az rest GET calls
306+
get_cmd1 = ' '.join(get_call1[0][0])
307+
self.assertIn('rest --method GET', get_cmd1)
308+
get_cmd2 = ' '.join(get_call2[0][0])
309+
self.assertIn('rest --method GET', get_cmd2)
310+
311+
def testCreatePostgresPremiumV2Fails(self):
312+
self.MockIssueCommand({
313+
'rest --method PUT': [(
314+
'',
315+
"'Azure-AsyncOperation': 'https://management.azure.com/async_op'",
316+
0,
317+
)],
318+
'rest --method GET': [('{"status": "Failed"}', '', 0)],
319+
})
320+
yaml_spec = inspect.cleandoc(f"""
321+
sysbench:
322+
relational_db:
323+
cloud: {provider_info.AZURE}
324+
engine: {sql_engine_utils.FLEXIBLE_SERVER_POSTGRES}
325+
engine_version: '13'
326+
db_tier: GeneralPurpose
327+
db_spec:
328+
{provider_info.AZURE}:
329+
machine_type: Standard_D2ds_v4
330+
zone: eastus
331+
db_disk_spec:
332+
{provider_info.AZURE}:
333+
disk_size: 128
334+
disk_type: PremiumV2_LRS
335+
vm_groups:
336+
clients:
337+
vm_spec: *default_dual_core
338+
disk_spec: *default_500_gb
339+
""")
340+
bm_spec = pkb_common_test_case.CreateBenchmarkSpecFromYaml(
341+
yaml_spec, 'sysbench'
342+
)
343+
bm_spec.ConstructRelationalDb()
344+
345+
with self.assertRaises(errors.Resource.CreationError):
346+
bm_spec.relational_db._Create()
347+
348+
def testCreatePostgresPremiumV2NoAsyncHeader(self):
349+
self.MockIssueCommand(
350+
{
351+
'rest --method PUT': [(
352+
'',
353+
'some other stderr',
354+
0,
355+
)]
356+
}
357+
)
358+
yaml_spec = inspect.cleandoc(f"""
359+
sysbench:
360+
relational_db:
361+
cloud: {provider_info.AZURE}
362+
engine: {sql_engine_utils.FLEXIBLE_SERVER_POSTGRES}
363+
engine_version: '13'
364+
db_tier: GeneralPurpose
365+
db_spec:
366+
{provider_info.AZURE}:
367+
machine_type: Standard_D2ds_v4
368+
zone: eastus
369+
db_disk_spec:
370+
{provider_info.AZURE}:
371+
disk_size: 128
372+
disk_type: PremiumV2_LRS
373+
vm_groups:
374+
clients:
375+
vm_spec: *default_dual_core
376+
disk_spec: *default_500_gb
377+
""")
378+
bm_spec = pkb_common_test_case.CreateBenchmarkSpecFromYaml(
379+
yaml_spec, 'sysbench'
380+
)
381+
bm_spec.ConstructRelationalDb()
382+
383+
with self.assertRaises(errors.Resource.CreationError):
384+
bm_spec.relational_db._Create()
385+
386+
234387
if __name__ == '__main__':
235388
unittest.main()

0 commit comments

Comments
 (0)