diff --git a/lisa/sut_orchestrator/azure/arm_template.bicep b/lisa/sut_orchestrator/azure/arm_template.bicep index 05366ec812..d7c92e6cfa 100644 --- a/lisa/sut_orchestrator/azure/arm_template.bicep +++ b/lisa/sut_orchestrator/azure/arm_template.bicep @@ -473,16 +473,18 @@ resource nodes_vms 'Microsoft.Compute/virtualMachines@2024-03-01' = [for i in ra imageReference: getImageReference(nodes[i]) osDisk: getVMOsDisk(nodes[i]) diskControllerType: (nodes[i].disk_controller_type == 'SCSI') ? null : nodes[i].disk_controller_type - dataDisks: concat( - map( - filter(range(0, length(data_disks)), j => !shouldAttachDataDisk(data_disks[j])), - j => getCreateDisk(data_disks[j], '${nodes[i].name}-data-disk-${j}', j) - ), - map( - filter(range(0, length(data_disks)), j => shouldAttachDataDisk(data_disks[j])), - j => getAttachDisk(data_disks[j], '${nodes[i].name}-data-disk-${j}', j) + dataDisks: empty(data_disks) + ? null + : concat( + map( + filter(range(0, length(data_disks)), j => !shouldAttachDataDisk(data_disks[j])), + j => getCreateDisk(data_disks[j], '${nodes[i].name}-data-disk-${j}', j) + ), + map( + filter(range(0, length(data_disks)), j => shouldAttachDataDisk(data_disks[j])), + j => getAttachDisk(data_disks[j], '${nodes[i].name}-data-disk-${j}', j) + ) ) - ) } networkProfile: { networkInterfaces: [for j in range(0, nodes[i].nic_count): { diff --git a/lisa/sut_orchestrator/azure/autogen_arm_template.json b/lisa/sut_orchestrator/azure/autogen_arm_template.json index 6fa5c2a110..266ec71ef3 100644 --- a/lisa/sut_orchestrator/azure/autogen_arm_template.json +++ b/lisa/sut_orchestrator/azure/autogen_arm_template.json @@ -816,7 +816,7 @@ "imageReference": "[__bicep.getImageReference(parameters('nodes')[range(0, variables('node_count'))[copyIndex()]])]", "osDisk": "[__bicep.getVMOsDisk(parameters('nodes')[range(0, variables('node_count'))[copyIndex()]])]", "diskControllerType": "[if(equals(parameters('nodes')[range(0, variables('node_count'))[copyIndex()]].disk_controller_type, 'SCSI'), null(), parameters('nodes')[range(0, variables('node_count'))[copyIndex()]].disk_controller_type)]", - "dataDisks": "[concat(map(filter(range(0, length(parameters('data_disks'))), lambda('j', not(__bicep.shouldAttachDataDisk(parameters('data_disks')[lambdaVariables('j')])))), lambda('j', __bicep.getCreateDisk(parameters('data_disks')[lambdaVariables('j')], format('{0}-data-disk-{1}', parameters('nodes')[range(0, variables('node_count'))[copyIndex()]].name, lambdaVariables('j')), lambdaVariables('j')))), map(filter(range(0, length(parameters('data_disks'))), lambda('j', __bicep.shouldAttachDataDisk(parameters('data_disks')[lambdaVariables('j')]))), lambda('j', __bicep.getAttachDisk(parameters('data_disks')[lambdaVariables('j')], format('{0}-data-disk-{1}', parameters('nodes')[range(0, variables('node_count'))[copyIndex()]].name, lambdaVariables('j')), lambdaVariables('j')))))]" + "dataDisks": "[if(empty(parameters('data_disks')), null(), concat(map(filter(range(0, length(parameters('data_disks'))), lambda('j', not(__bicep.shouldAttachDataDisk(parameters('data_disks')[lambdaVariables('j')])))), lambda('j', __bicep.getCreateDisk(parameters('data_disks')[lambdaVariables('j')], format('{0}-data-disk-{1}', parameters('nodes')[range(0, variables('node_count'))[copyIndex()]].name, lambdaVariables('j')), lambdaVariables('j')))), map(filter(range(0, length(parameters('data_disks'))), lambda('j', __bicep.shouldAttachDataDisk(parameters('data_disks')[lambdaVariables('j')]))), lambda('j', __bicep.getAttachDisk(parameters('data_disks')[lambdaVariables('j')], format('{0}-data-disk-{1}', parameters('nodes')[range(0, variables('node_count'))[copyIndex()]].name, lambdaVariables('j')), lambdaVariables('j'))))))]" }, "networkProfile": { "copy": [ diff --git a/lisa/sut_orchestrator/azure/common.py b/lisa/sut_orchestrator/azure/common.py index cb4692ea46..5eceb8c408 100644 --- a/lisa/sut_orchestrator/azure/common.py +++ b/lisa/sut_orchestrator/azure/common.py @@ -2971,6 +2971,7 @@ def check_or_create_gallery_image_version( vhd_resource_group_name: str, vhd_storage_account_name: str, gallery_image_target_regions: List[str], + data_vhd_paths: Optional[List[Dict[str, Any]]] = None, ) -> None: try: compute_client = get_compute_client(platform) @@ -2992,7 +2993,7 @@ def check_or_create_gallery_image_version( "storage_account_type": storage_account_type, } ) - image_version_post_body = { + image_version_post_body: Dict[str, Any] = { "location": gallery_image_location, "publishing_profile": {"target_regions": target_regions}, "storageProfile": { @@ -3010,6 +3011,50 @@ def check_or_create_gallery_image_version( }, }, } + + if data_vhd_paths: + data_disk_images: List[Dict[str, Any]] = [] + assigned_luns: set[int] = set() + for index, data_vhd_path_item in enumerate(data_vhd_paths): + lun = index + raw_path = data_vhd_path_item.get("url") + if not isinstance(raw_path, str) or not raw_path: + continue + data_vhd_path = raw_path + + raw_lun = data_vhd_path_item.get("lun") + if isinstance(raw_lun, int) and raw_lun >= 0: + lun = raw_lun + elif isinstance(raw_lun, str) and raw_lun.strip().isdigit(): + lun = int(raw_lun.strip()) + + if lun in assigned_luns: + raise LisaException( + f"duplicated data disk lun '{lun}' in data_vhd_paths" + ) + assigned_luns.add(lun) + + data_vhd_details = get_vhd_details(platform, data_vhd_path) + data_disk_images.append( + { + "lun": lun, + "hostCaching": host_caching_type, + "source": { + "uri": data_vhd_path, + "storageAccountId": ( + f"/subscriptions/{platform.subscription_id}/" + f"resourceGroups/" + f"{data_vhd_details['resource_group_name']}" + "/providers/Microsoft.Storage/storageAccounts/" + f"{data_vhd_details['account_name']}" + ), + }, + } + ) + image_version_post_body["storageProfile"][ + "dataDiskImages" + ] = data_disk_images + operation = compute_client.gallery_image_versions.begin_create_or_update( gallery_resource_group_name, gallery_name, diff --git a/lisa/sut_orchestrator/azure/transformers.py b/lisa/sut_orchestrator/azure/transformers.py index 7576d9809e..a9993b0f55 100644 --- a/lisa/sut_orchestrator/azure/transformers.py +++ b/lisa/sut_orchestrator/azure/transformers.py @@ -24,6 +24,7 @@ get_date_str, get_datetime_path, ) +from lisa.secret import PATTERN_URL, add_secret from .common import ( AZURE_SHARED_RG_NAME, @@ -481,9 +482,21 @@ class SigTransformerSchema(schema.Transformer): azure_sig_url: shared_gallery """ - # raw vhd URL, it can be the blob under the same subscription of SIG - # or SASURL - vhd: str = field(default="", metadata=field_metadata(required=True)) + # Raw VHD URL or VHD schema object. String form keeps backward compatibility: + # vhd: "https://.../os.vhd" + # Object form supports data disk VHDs: + # vhd: + # vhd_path: "https://.../os.vhd" + # data_vhd_paths: + # - data_vhd: + # lun: 0 + # url: "https://.../data0.vhd" + # - data_vhd: + # lun: 1 + # url: "https://.../data1.vhd" + vhd: Union[str, Dict[Any, Any]] = field( + default="", metadata=field_metadata(required=True) + ) # if not specify gallery_resource_group_name, use shared resource group name gallery_resource_group_name: str = field(default=AZURE_SHARED_RG_NAME) # if not specified, will use the first location of gallery image @@ -610,18 +623,24 @@ def _internal_run(self) -> Dict[str, Any]: runbook.gallery_resource_group_location = image_location if not runbook.gallery_location: runbook.gallery_location = image_location + + source_vhd_path, source_data_vhd_paths = self._resolve_vhd_sources(runbook) vhd_path = get_deployable_storage_path( - platform, runbook.vhd, image_location, self._log - ) - vhd_details = get_vhd_details(platform, vhd_path) - check_blob_exist( - platform=platform, - account_name=vhd_details["account_name"], - container_name=vhd_details["container_name"], - resource_group_name=vhd_details["resource_group_name"], - blob_name=vhd_details["blob_name"], - raise_error=True, + platform, source_vhd_path, image_location, self._log ) + vhd_details = self._check_blob_exists(platform, vhd_path) + + data_vhd_paths: List[Dict[str, Any]] = [] + for source_data_vhd in source_data_vhd_paths: + source_data_vhd_path = source_data_vhd["url"] + data_vhd_path = get_deployable_storage_path( + platform, source_data_vhd_path, image_location, self._log + ) + self._check_blob_exists(platform, data_vhd_path) + data_vhd: Dict[str, Any] = {"url": data_vhd_path} + if "lun" in source_data_vhd: + data_vhd["lun"] = source_data_vhd["lun"] + data_vhd_paths.append(data_vhd) # Get features from marketplace image if specified features = self._get_image_features(platform, runbook.marketplace_source) @@ -696,6 +715,7 @@ def _internal_run(self) -> Dict[str, Any]: vhd_details["resource_group_name"], vhd_details["account_name"], runbook.gallery_image_location, + data_vhd_paths=data_vhd_paths, ) sig_url = ( @@ -707,6 +727,75 @@ def _internal_run(self) -> Dict[str, Any]: self._log.info(f"SIG Url: {sig_url}") return {self.__sig_name: sig_url} + def _check_blob_exists( + self, platform: AzurePlatform, vhd_path: str + ) -> Dict[str, str]: + vhd_details = cast(Dict[str, str], get_vhd_details(platform, vhd_path)) + check_blob_exist( + platform=platform, + account_name=vhd_details["account_name"], + container_name=vhd_details["container_name"], + resource_group_name=vhd_details["resource_group_name"], + blob_name=vhd_details["blob_name"], + raise_error=True, + ) + return vhd_details + + def _resolve_vhd_sources( + self, runbook: SigTransformerSchema + ) -> tuple[str, List[Dict[str, Any]]]: + data_vhd_paths: List[Dict[str, Any]] = [] + + def _add_data_vhd(url: str, lun: Optional[int] = None) -> None: + normalized_url = url.strip() + if not normalized_url: + return + add_secret(normalized_url, PATTERN_URL) + item: Dict[str, Any] = {"url": normalized_url} + if lun is not None: + item["lun"] = lun + data_vhd_paths.append(item) + + def _parse_lun(raw_lun: Any) -> Optional[int]: + if isinstance(raw_lun, int) and raw_lun >= 0: + return raw_lun + if isinstance(raw_lun, str): + raw_lun = raw_lun.strip() + if raw_lun.isdigit(): + return int(raw_lun) + return None + + if isinstance(runbook.vhd, str): + vhd_path = runbook.vhd + elif isinstance(runbook.vhd, dict): + raw_vhd_path = runbook.vhd.get("vhd_path", "") + vhd_path = raw_vhd_path.strip() if isinstance(raw_vhd_path, str) else "" + + raw_data_vhd_paths = runbook.vhd.get("data_vhd_paths") + if isinstance(raw_data_vhd_paths, list): + for item in raw_data_vhd_paths: + if not isinstance(item, dict): + continue + + # Supported format: + # - data_vhd: + # lun: 0 + # url: "https://.../data0.vhd" + raw_data_vhd = item.get("data_vhd") + if isinstance(raw_data_vhd, dict): + url = raw_data_vhd.get("url") + if isinstance(url, str) and url.strip(): + _add_data_vhd(url, _parse_lun(raw_data_vhd.get("lun"))) + else: + raise LisaException( + f"unsupported type for transformer vhd: {type(runbook.vhd)}" + ) + + if not vhd_path: + raise LisaException("vhd or vhd.vhd_path must not be empty.") + + return vhd_path, data_vhd_paths + def _get_image_features( self, platform: AzurePlatform, marketplace: str ) -> Dict[str, Any]: