1from io import StringIO 2from typing import TYPE_CHECKING, Any 3 4from ruamel.yaml import YAML 5 6from lava.utils.lava_farm import LavaFarm, get_lava_farm 7from lava.utils.ssh_job_definition import ( 8 generate_docker_test, 9 generate_dut_test, 10 wrap_boot_action, 11 wrap_final_deploy_action, 12) 13from lava.utils.uart_job_definition import ( 14 fastboot_boot_action, 15 fastboot_deploy_actions, 16 tftp_boot_action, 17 tftp_deploy_actions, 18 qemu_boot_action, 19 qemu_deploy_actions, 20 uart_test_actions, 21) 22 23if TYPE_CHECKING: 24 from lava.lava_job_submitter import LAVAJobSubmitter 25 26from .constants import FORCE_UART, JOB_PRIORITY, NUMBER_OF_ATTEMPTS_LAVA_BOOT 27 28 29class LAVAJobDefinition: 30 """ 31 This class is responsible for generating the YAML payload to submit a LAVA 32 job. 33 """ 34 35 def __init__(self, job_submitter: "LAVAJobSubmitter") -> None: 36 self.job_submitter: "LAVAJobSubmitter" = job_submitter 37 # NFS args provided by LAVA 38 self.lava_nfs_args: str = "root=/dev/nfs rw nfsroot=$NFS_SERVER_IP:$NFS_ROOTFS,tcp,hard,v3 ip=dhcp" 39 # extra_nfsroot_args appends to cmdline 40 self.extra_nfsroot_args: str = " init=/init rootwait usbcore.quirks=0bda:8153:k" 41 42 def has_ssh_support(self) -> bool: 43 if FORCE_UART: 44 return False 45 46 # Only Collabora's farm supports to run docker container as a LAVA actions, 47 # which is required to follow the job in a SSH section 48 current_farm = get_lava_farm() 49 50 return current_farm == LavaFarm.COLLABORA 51 52 def generate_lava_yaml_payload(self) -> dict[str, Any]: 53 """ 54 Generates a YAML payload for submitting a LAVA job, based on the provided arguments. 55 56 Args: 57 None 58 59 Returns: 60 a dictionary containing the values generated by the `generate_metadata` function and the 61 actions for the LAVA job submission. 62 """ 63 args = self.job_submitter 64 nfsrootfs = { 65 "url": f"{args.rootfs_url}", 66 "compression": "zstd", 67 "format": "tar", 68 "overlays": args._overlays, 69 } 70 values = self.generate_metadata() 71 72 init_stage1_steps = self.init_stage1_steps() 73 jwt_steps = self.jwt_steps() 74 75 deploy_actions = [] 76 boot_action = [] 77 test_actions = uart_test_actions(args, init_stage1_steps, jwt_steps) 78 79 if args.boot_method == "fastboot": 80 deploy_actions = fastboot_deploy_actions(self, nfsrootfs) 81 boot_action = fastboot_boot_action(args) 82 elif args.boot_method == "qemu-nfs": 83 deploy_actions = qemu_deploy_actions(self, nfsrootfs) 84 boot_action = qemu_boot_action(args) 85 else: # tftp 86 deploy_actions = tftp_deploy_actions(self, nfsrootfs) 87 boot_action = tftp_boot_action(args) 88 89 if self.has_ssh_support(): 90 wrap_final_deploy_action(deploy_actions[-1]) 91 # SSH jobs use namespaces to differentiate between the DUT and the 92 # docker container. Every LAVA action needs an explicit namespace, when we are not using 93 # the default one. 94 for deploy_action in deploy_actions: 95 deploy_action["namespace"] = "dut" 96 wrap_boot_action(boot_action) 97 test_actions = ( 98 generate_dut_test(args, init_stage1_steps), 99 generate_docker_test(args, jwt_steps), 100 ) 101 102 values["actions"] = [ 103 *[{"deploy": d} for d in deploy_actions], 104 {"boot": boot_action}, 105 *[{"test": t} for t in test_actions], 106 ] 107 108 return values 109 110 def generate_lava_job_definition(self) -> str: 111 """ 112 Generates a LAVA job definition in YAML format and returns it as a string. 113 114 Returns: 115 a string representation of the job definition generated by analysing job submitter 116 arguments and environment variables 117 """ 118 job_stream = StringIO() 119 yaml = YAML() 120 yaml.width = 4096 121 yaml.dump(self.generate_lava_yaml_payload(), job_stream) 122 return job_stream.getvalue() 123 124 def consume_lava_tags_args(self, values: dict[str, Any]): 125 # python-fire parses --lava-tags without arguments as True 126 if isinstance(self.job_submitter.lava_tags, tuple): 127 values["tags"] = self.job_submitter.lava_tags 128 # python-fire parses "tag-1,tag2" as str and "tag1,tag2" as tuple 129 # even if the -- --separator is something other than '-' 130 elif isinstance(self.job_submitter.lava_tags, str): 131 # Split string tags by comma, removing any trailing commas 132 values["tags"] = self.job_submitter.lava_tags.rstrip(",").split(",") 133 # Ensure tags are always a list of non-empty strings 134 if "tags" in values: 135 values["tags"] = [tag for tag in values["tags"] if tag] 136 # Remove empty tags 137 if "tags" in values and not values["tags"]: 138 del values["tags"] 139 140 def generate_metadata(self) -> dict[str, Any]: 141 # General metadata and permissions 142 values = { 143 "job_name": f"{self.job_submitter.project_name}: {self.job_submitter.pipeline_info}", 144 "device_type": self.job_submitter.device_type, 145 "visibility": {"group": [self.job_submitter.visibility_group]}, 146 "priority": JOB_PRIORITY, 147 "context": {"extra_nfsroot_args": self.extra_nfsroot_args}, 148 "timeouts": { 149 "job": {"minutes": self.job_submitter.job_timeout_min}, 150 "actions": { 151 "depthcharge-retry": { 152 # Could take between 1 and 1.5 min in slower boots 153 "minutes": 4 154 }, 155 "depthcharge-start": { 156 # Should take less than 1 min. 157 "minutes": 1, 158 }, 159 "depthcharge-action": { 160 # This timeout englobes the entire depthcharge timing, 161 # including retries 162 "minutes": 5 163 * NUMBER_OF_ATTEMPTS_LAVA_BOOT, 164 }, 165 }, 166 }, 167 } 168 169 self.consume_lava_tags_args(values) 170 171 # QEMU lava jobs mandate proper arch value in the context 172 if self.job_submitter.boot_method == "qemu-nfs": 173 values["context"]["arch"] = self.job_submitter.mesa_job_name.split(":")[1] 174 175 return values 176 177 def attach_kernel_and_dtb(self, deploy_field): 178 if self.job_submitter.kernel_image_type: 179 deploy_field["kernel"]["type"] = self.job_submitter.kernel_image_type 180 if self.job_submitter.dtb_filename: 181 deploy_field["dtb"] = { 182 "url": f"{self.job_submitter.kernel_url_prefix}/" 183 f"{self.job_submitter.dtb_filename}.dtb" 184 } 185 186 def attach_external_modules(self, deploy_field): 187 if self.job_submitter.kernel_external: 188 deploy_field["modules"] = { 189 "url": f"{self.job_submitter.kernel_url_prefix}/modules.tar.zst", 190 "compression": "zstd" 191 } 192 193 def jwt_steps(self): 194 """ 195 This function is responsible for setting up the SSH server in the DUT and to 196 export the first boot environment to a file. 197 """ 198 # Pre-process the JWT 199 jwt_steps = [ 200 "set -e", 201 ] 202 203 # If the JWT file is provided, we will use it to authenticate with the cloud 204 # storage provider and will hide it from the job output in Gitlab. 205 if self.job_submitter.jwt_file: 206 with open(self.job_submitter.jwt_file) as jwt_file: 207 jwt_steps += [ 208 "set +x # HIDE_START", 209 f'echo -n "{jwt_file.read()}" > "{self.job_submitter.jwt_file}"', 210 "set -x # HIDE_END", 211 f'echo "export S3_JWT_FILE={self.job_submitter.jwt_file}" >> /set-job-env-vars.sh', 212 ] 213 else: 214 jwt_steps += [ 215 "echo Could not find jwt file, disabling S3 requests...", 216 "sed -i '/S3_RESULTS_UPLOAD/d' /set-job-env-vars.sh", 217 ] 218 219 return jwt_steps 220 221 def init_stage1_steps(self) -> list[str]: 222 run_steps = [] 223 # job execution script: 224 # - inline .gitlab-ci/common/init-stage1.sh 225 # - fetch and unpack per-pipeline build artifacts from build job 226 # - fetch and unpack per-job environment from lava-submit.sh 227 # - exec .gitlab-ci/common/init-stage2.sh 228 229 with open(self.job_submitter.first_stage_init, "r") as init_sh: 230 # For vmware farm, patch nameserver as 8.8.8.8 is off limit. 231 # This is temporary and will be reverted once the farm is moved. 232 if self.job_submitter.mesa_job_name.startswith("vmware-"): 233 run_steps += [x.rstrip().replace("nameserver 8.8.8.8", "nameserver 192.19.189.10") for x in init_sh if not x.startswith("#") and x.rstrip()] 234 else: 235 run_steps += [x.rstrip() for x in init_sh if not x.startswith("#") and x.rstrip()] 236 237 # We cannot distribute the Adreno 660 shader firmware inside rootfs, 238 # since the license isn't bundled inside the repository 239 if self.job_submitter.device_type == "sm8350-hdk": 240 run_steps.append( 241 "curl -L --retry 4 -f --retry-all-errors --retry-delay 60 " 242 + "https://github.com/allahjasif1990/hdk888-firmware/raw/main/a660_zap.mbn " 243 + '-o "/lib/firmware/qcom/sm8350/a660_zap.mbn"' 244 ) 245 246 run_steps.append("export CURRENT_SECTION=dut_boot") 247 248 return run_steps 249