Skip to content

Commit 6837420

Browse files
authored
Merge pull request #71 from release-engineering/arm64
Azure: Add support for ARM64 Images [SPSTRAT-467]
2 parents ea8447f + e2351b4 commit 6837420

File tree

4 files changed

+341
-27
lines changed

4 files changed

+341
-27
lines changed

cloudpub/ms_azure/utils.py

Lines changed: 62 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# SPDX-License-Identifier: GPL-3.0-or-later
22
import logging
33
from operator import attrgetter
4-
from typing import Dict, List, Optional, Tuple
4+
from typing import Any, Dict, List, Optional, Tuple
55

66
from deepdiff import DeepDiff
77

@@ -69,7 +69,22 @@ def __init__(
6969
super(AzurePublishingMetadata, self).__init__(**kwargs)
7070
self.__validate()
7171
# Adjust the x86_64 architecture string for Azure
72-
self.architecture = "x64" if self.architecture == "x86_64" else self.architecture
72+
arch = self.__convert_arch(self.architecture)
73+
self.architecture = arch
74+
75+
def __setattr__(self, name: str, value: Any) -> None:
76+
if name == "architecture":
77+
arch = self.__convert_arch(value)
78+
value = arch
79+
return super().__setattr__(name, value)
80+
81+
@staticmethod
82+
def __convert_arch(arch: str) -> str:
83+
converter = {
84+
"x86_64": "x64",
85+
"aarch64": "arm64",
86+
}
87+
return converter.get(arch, "") or arch
7388

7489
def __validate(self):
7590
mandatory = [
@@ -91,9 +106,10 @@ def __validate(self):
91106
def get_image_type_mapping(architecture: str, generation: str) -> str:
92107
"""Return the image type required by VMImageDefinition."""
93108
gen_map = {
94-
"V1": f"{architecture}Gen1",
95109
"V2": f"{architecture}Gen2",
96110
}
111+
if architecture == "x64":
112+
gen_map.update({"V1": f"{architecture}Gen1"})
97113
return gen_map.get(generation, "")
98114

99115

@@ -185,6 +201,17 @@ def is_azure_job_not_complete(job_details: ConfigureStatus) -> bool:
185201
return False
186202

187203

204+
def is_legacy_gen_supported(metadata: AzurePublishingMetadata) -> bool:
205+
"""Return True when the legagy V1 SKU is supported, False otherwise.
206+
207+
Args:
208+
metadata: The incoming publishing metadata.
209+
Returns:
210+
bool: True when V1 is supported, False otherwise.
211+
"""
212+
return metadata.architecture == "x64" and metadata.support_legacy
213+
214+
188215
def prepare_vm_images(
189216
metadata: AzurePublishingMetadata,
190217
gen1: Optional[VMImageDefinition],
@@ -226,7 +253,7 @@ def prepare_vm_images(
226253
if metadata.generation == "V2":
227254
# In this case we need to set a V2 SAS URI
228255
gen2_new = VMImageDefinition.from_json(json_gen2)
229-
if metadata.support_legacy: # and in this case a V1 as well
256+
if is_legacy_gen_supported(metadata): # and in this case a V1 as well
230257
gen1_new = VMImageDefinition.from_json(json_gen1)
231258
return [gen2_new, gen1_new]
232259
return [gen2_new]
@@ -235,13 +262,25 @@ def prepare_vm_images(
235262
return [VMImageDefinition.from_json(json_gen1)]
236263

237264

265+
def _len_vm_images(disk_versions: List[DiskVersion]) -> int:
266+
count = 0
267+
for disk_version in disk_versions:
268+
count = count + len(disk_version.vm_images)
269+
return count
270+
271+
238272
def _build_skus(
239273
disk_versions: List[DiskVersion],
240274
default_gen: str,
241275
alt_gen: str,
242276
plan_name: str,
243277
security_type: Optional[List[str]] = None,
244278
) -> List[VMISku]:
279+
def get_skuid(arch):
280+
if arch == "x64":
281+
return plan_name
282+
return f"{plan_name}-{arch.lower()}"
283+
245284
sku_mapping: Dict[str, str] = {}
246285
# Update the SKUs for each image in DiskVersions if needed
247286
for disk_version in disk_versions:
@@ -254,10 +293,11 @@ def _build_skus(
254293
new_img_alt_type = get_image_type_mapping(arch, alt_gen)
255294

256295
# we just want to add SKU whenever it's not set
296+
skuid = get_skuid(arch)
257297
if vmid.image_type == new_img_type:
258-
sku_mapping.setdefault(new_img_type, plan_name)
298+
sku_mapping.setdefault(new_img_type, skuid)
259299
elif vmid.image_type == new_img_alt_type:
260-
sku_mapping.setdefault(new_img_alt_type, f"{plan_name}-gen{alt_gen[1:]}")
300+
sku_mapping.setdefault(new_img_alt_type, f"{skuid}-gen{alt_gen[1:]}")
261301

262302
# Return the expected SKUs list
263303
res = [
@@ -267,6 +307,15 @@ def _build_skus(
267307
return sorted(res, key=attrgetter("id"))
268308

269309

310+
def _get_security_type(old_skus: List[VMISku]) -> Optional[List[str]]:
311+
# The security type may exist only for x64 Gen2, so it iterates over all gens to find it
312+
# Get the security type for all gens
313+
for osku in old_skus:
314+
if osku.security_type is not None:
315+
return osku.security_type
316+
return None
317+
318+
270319
def update_skus(
271320
disk_versions: List[DiskVersion],
272321
generation: str,
@@ -295,21 +344,18 @@ def update_skus(
295344
disk_versions, default_gen=generation, alt_gen=alt_gen, plan_name=plan_name
296345
)
297346

298-
# If we have SKUs for both genenerations we don't need to update them as they're already
347+
# If we have SKUs for each image we don't need to update them as they're already
299348
# properly set.
300-
if len(old_skus) == 2:
349+
if len(old_skus) == _len_vm_images(disk_versions):
301350
return old_skus
302351

303352
# Update SKUs to create the alternate gen.
304-
# The security type may exist only for Gen2, so it iterates over all gens to find it
305-
security_type = None
306-
# The alternate plan name ends with the suffix "-genX" and we can't change that once
353+
security_type = _get_security_type(old_skus)
354+
355+
# The alternate plan for x64 name ends with the suffix "-genX" and we can't change that once
307356
# the offer is live, otherwise it will raise "BadRequest" with the message:
308357
# "The property 'PlanId' is locked by a previous submission".
309358
osku = old_skus[0]
310-
# Get the security type for all gens
311-
if osku.security_type is not None:
312-
security_type = osku.security_type
313359

314360
# Default Gen2 cases
315361
if osku.image_type.endswith("Gen1") and osku.id.endswith("gen1"):
@@ -354,7 +400,7 @@ def create_disk_version_from_scratch(
354400
"source": source.to_json(),
355401
}
356402
]
357-
if metadata.support_legacy:
403+
if is_legacy_gen_supported(metadata):
358404
vm_images.append(
359405
{
360406
"imageType": get_image_type_mapping(metadata.architecture, "V1"),
@@ -463,7 +509,7 @@ def create_vm_image_definitions(
463509
source=source.to_json(),
464510
)
465511
)
466-
if metadata.support_legacy: # Only True when metadata.generation == V2
512+
if is_legacy_gen_supported(metadata):
467513
vm_images.append(
468514
VMImageDefinition(
469515
image_type=get_image_type_mapping(metadata.architecture, "V1"),

tests/ms_azure/conftest.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,14 @@ def gen2_image(vmimage_source) -> Dict[str, Any]:
333333
}
334334

335335

336+
@pytest.fixture
337+
def arm_image(vmimage_source) -> Dict[str, Any]:
338+
return {
339+
"imageType": "arm64Gen2",
340+
"source": vmimage_source,
341+
}
342+
343+
336344
@pytest.fixture
337345
def disk_version(gen1_image: Dict[str, Any], gen2_image: Dict[str, Any]) -> Dict[str, Any]:
338346
return {
@@ -342,6 +350,15 @@ def disk_version(gen1_image: Dict[str, Any], gen2_image: Dict[str, Any]) -> Dict
342350
}
343351

344352

353+
@pytest.fixture
354+
def disk_version_arm64(arm_image):
355+
return {
356+
"versionNumber": "2.1.0",
357+
"vmImages": [arm_image],
358+
"lifecycleState": "generallyAvailable",
359+
}
360+
361+
345362
@pytest.fixture
346363
def technical_config(disk_version: Dict[str, Any]) -> Dict[str, Any]:
347364
return {
@@ -540,11 +557,21 @@ def gen2_image_obj(gen2_image: Dict[str, Any]) -> VMImageDefinition:
540557
return VMImageDefinition.from_json(gen2_image)
541558

542559

560+
@pytest.fixture
561+
def arm_image_obj(arm_image: Dict[str, Any]) -> VMImageDefinition:
562+
return VMImageDefinition.from_json(arm_image)
563+
564+
543565
@pytest.fixture
544566
def disk_version_obj(disk_version: Dict[str, Any]) -> DiskVersion:
545567
return DiskVersion.from_json(disk_version)
546568

547569

570+
@pytest.fixture
571+
def disk_version_arm64_obj(disk_version_arm64: Dict[str, Any]) -> DiskVersion:
572+
return DiskVersion.from_json(disk_version_arm64)
573+
574+
548575
@pytest.fixture
549576
def vmimage_source_obj(vmimage_source: Dict[str, Any]) -> VMImageSource:
550577
return VMImageSource.from_json(vmimage_source)

tests/ms_azure/test_service.py

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1327,7 +1327,7 @@ def test_is_submission_in_preview(
13271327
@mock.patch("cloudpub.ms_azure.service.create_disk_version_from_scratch")
13281328
@mock.patch("cloudpub.ms_azure.AzureService.filter_product_resources")
13291329
@mock.patch("cloudpub.ms_azure.AzureService.get_product_plan_by_name")
1330-
def test_publish_live(
1330+
def test_publish_live_x64_only(
13311331
self,
13321332
mock_getprpl_name: mock.MagicMock,
13331333
mock_filter: mock.MagicMock,
@@ -1405,3 +1405,93 @@ def test_publish_live(
14051405
]
14061406
mock_submit.assert_has_calls(submit_calls)
14071407
mock_ensure_publish.assert_called_once_with(product_obj.id)
1408+
1409+
@mock.patch("cloudpub.ms_azure.AzureService.ensure_can_publish")
1410+
@mock.patch("cloudpub.ms_azure.AzureService.get_submission_state")
1411+
@mock.patch("cloudpub.ms_azure.AzureService.diff_offer")
1412+
@mock.patch("cloudpub.ms_azure.AzureService.configure")
1413+
@mock.patch("cloudpub.ms_azure.AzureService.submit_to_status")
1414+
@mock.patch("cloudpub.ms_azure.utils.prepare_vm_images")
1415+
@mock.patch("cloudpub.ms_azure.service.is_sas_present")
1416+
@mock.patch("cloudpub.ms_azure.service.create_disk_version_from_scratch")
1417+
@mock.patch("cloudpub.ms_azure.AzureService.filter_product_resources")
1418+
@mock.patch("cloudpub.ms_azure.AzureService.get_product_plan_by_name")
1419+
def test_publish_live_arm64_only(
1420+
self,
1421+
mock_getprpl_name: mock.MagicMock,
1422+
mock_filter: mock.MagicMock,
1423+
mock_disk_scratch: mock.MagicMock,
1424+
mock_is_sas: mock.MagicMock,
1425+
mock_prep_img: mock.MagicMock,
1426+
mock_submit: mock.MagicMock,
1427+
mock_configure: mock.MagicMock,
1428+
mock_diff_offer: mock.MagicMock,
1429+
mock_getsubst: mock.MagicMock,
1430+
mock_ensure_publish: mock.MagicMock,
1431+
product_obj: Product,
1432+
plan_summary_obj: PlanSummary,
1433+
metadata_azure_obj: AzurePublishingMetadata,
1434+
technical_config_obj: VMIPlanTechConfig,
1435+
disk_version_arm64_obj: DiskVersion,
1436+
submission_obj: ProductSubmission,
1437+
azure_service: AzureService,
1438+
) -> None:
1439+
metadata_azure_obj.overwrite = False
1440+
metadata_azure_obj.keepdraft = False
1441+
metadata_azure_obj.support_legacy = True
1442+
metadata_azure_obj.destination = "example-product/plan-1"
1443+
metadata_azure_obj.disk_version = "2.1.0"
1444+
metadata_azure_obj.architecture = "aarch64"
1445+
technical_config_obj.disk_versions = [disk_version_arm64_obj]
1446+
mock_getprpl_name.return_value = product_obj, plan_summary_obj
1447+
mock_filter.side_effect = [
1448+
[technical_config_obj],
1449+
[submission_obj],
1450+
]
1451+
mock_getsubst.side_effect = ["preview", "live"]
1452+
mock_res_preview = mock.MagicMock()
1453+
mock_res_live = mock.MagicMock()
1454+
mock_res_preview.job_result = mock_res_live.job_result = "succeeded"
1455+
mock_submit.side_effect = [mock_res_preview, mock_res_live]
1456+
mock_is_sas.return_value = False
1457+
expected_source = VMImageSource(
1458+
source_type="sasUri",
1459+
os_disk=OSDiskURI(uri=metadata_azure_obj.image_path).to_json(),
1460+
data_disks=[],
1461+
)
1462+
disk_version_arm64_obj.vm_images[0] = VMImageDefinition(
1463+
image_type=get_image_type_mapping(metadata_azure_obj.architecture, "V2"),
1464+
source=expected_source.to_json(),
1465+
)
1466+
mock_prep_img.return_value = deepcopy(
1467+
disk_version_arm64_obj.vm_images
1468+
) # During submit it will pop the disk_versions
1469+
technical_config_obj.disk_versions = [disk_version_arm64_obj]
1470+
1471+
# Test
1472+
azure_service.publish(metadata_azure_obj)
1473+
mock_getprpl_name.assert_called_once_with("example-product", "plan-1")
1474+
filter_calls = [
1475+
mock.call(product=product_obj, resource="virtual-machine-plan-technical-configuration"),
1476+
mock.call(product=product_obj, resource="submission"),
1477+
]
1478+
mock_filter.assert_has_calls(filter_calls)
1479+
mock_is_sas.assert_called_once_with(
1480+
technical_config_obj,
1481+
metadata_azure_obj.image_path,
1482+
)
1483+
mock_prep_img.assert_called_once_with(
1484+
metadata=metadata_azure_obj,
1485+
gen1=None,
1486+
gen2=disk_version_arm64_obj.vm_images[0],
1487+
source=expected_source,
1488+
)
1489+
mock_disk_scratch.assert_not_called()
1490+
mock_diff_offer.assert_called_once_with(product_obj)
1491+
mock_configure.assert_called_once_with(resource=technical_config_obj)
1492+
submit_calls = [
1493+
mock.call(product_id=product_obj.id, status="preview"),
1494+
mock.call(product_id=product_obj.id, status="live"),
1495+
]
1496+
mock_submit.assert_has_calls(submit_calls)
1497+
mock_ensure_publish.assert_called_once_with(product_obj.id)

0 commit comments

Comments
 (0)