• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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