musca.py 11.5 KB
Newer Older
Dean Birch's avatar
Dean Birch committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# Copyright (C) 2019 Arm Limited
#
# Author: Dean Birch <dean.birch@arm.com>
#
# This file is part of LAVA Dispatcher.
#
# LAVA Dispatcher is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# LAVA Dispatcher is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along
# with this program; if not, see <http://www.gnu.org/licenses>.

# List just the subclasses supported for this base strategy
# imported by the parser to populate the list of subclasses.

import os
import shutil
from lava_dispatcher.action import Action, Pipeline
from lava_common.exceptions import InfrastructureError
from lava_dispatcher.actions.deploy.vemsd import MountDeviceMassStorageDevice
from lava_dispatcher.actions.deploy.vemsd import UnmountVExpressMassStorageDevice
from lava_dispatcher.logical import Deployment, RetryAction
from lava_dispatcher.actions.deploy.download import DownloaderAction
from lava_dispatcher.connections.serial import DisconnectDevice
from lava_dispatcher.power import ResetDevice
Dean Birch's avatar
Dean Birch committed
34
from lava_dispatcher.utils.udev import wait_udev_changed_event, wait_udev_event
Dean Birch's avatar
Dean Birch committed
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85


class Musca(Deployment):
    """
    Strategy class for a booting Arm Musca devices.
    Downloads an image and deploys to the board.
    """

    compatibility = 1
    name = "musca"

    def __init__(self, parent, parameters):
        super().__init__(parent)
        self.action = MuscaAction()
        self.action.section = self.action_type
        self.action.job = self.job
        parent.add_action(self.action, parameters)

    @classmethod
    def accepts(cls, device, parameters):
        if "musca" not in device["actions"]["deploy"]["methods"]:
            return False, '"musca" was not in the device configuration deploy methods'
        if "to" not in parameters:
            return False, '"to" was not in parameters'
        if parameters["to"] != "musca":
            return False, '"to" was not "musca"'
        if "board_id" not in device:
            return False, '"board_id" is not in the device configuration'
        return True, "accepted"


class MuscaAction(RetryAction):
    """
    Action for deploying software to a Musca
    """

    name = "musca-deploy"
    description = "deploy image to Musca device"
    summary = "Musca device image deployment"

    def validate(self):
        super().validate()
        if "images" not in self.parameters:
            self.errors = "Missing 'images'"
            return
        images = self.parameters["images"]
        if "test_binary" not in images:
            self.errors = "Missing 'test_binary'"

    def populate(self, parameters):
        download_dir = self.mkdtemp()
86
        self.pipeline = Pipeline(parent=self, job=self.job, parameters=parameters)
Dean Birch's avatar
Dean Birch committed
87
88
        # Musca will autoboot previously deployed binaries
        # Therefore disconnect serial to avoid clutter.
89
        self.pipeline.add_action(DisconnectDevice())
Dean Birch's avatar
Dean Birch committed
90
91
92
        # If we don't run with a strict schema, it is possible to pass validation with warnings
        # even without the required 'test_binary' field.
        # Therefore, ensure the DownloaderAction.populate does not fail, and catch this at validate step.
93
94
        image_params = parameters.get("images", {}).get("test_binary")
        if image_params:
95
            self.pipeline.add_action(
96
                DownloaderAction("test_binary", path=download_dir, params=image_params)
Dean Birch's avatar
Dean Birch committed
97
98
            )
        # Turn on
99
        self.pipeline.add_action(ResetDevice())
Dean Birch's avatar
Dean Birch committed
100
        # Wait for storage
101
        self.pipeline.add_action(WaitMuscaMassStorageAction())
Dean Birch's avatar
Dean Birch committed
102
103

        # Deploy test binary
104
105
106
        self.pipeline.add_action(MountMuscaMassStorageDevice())
        self.pipeline.add_action(DeployMuscaTestBinary())
        self.pipeline.add_action(UnmountMuscaMassStorageDevice())
Dean Birch's avatar
Dean Birch committed
107
108

        # Check for FAIL.TXT to check if we were successful
109
110
111
112
        self.pipeline.add_action(WaitMuscaMassStorageAction(udev_action="change"))
        self.pipeline.add_action(MountMuscaMassStorageDevice())
        self.pipeline.add_action(CheckMuscaFlashAction())
        self.pipeline.add_action(UnmountMuscaMassStorageDevice())
Dean Birch's avatar
Dean Birch committed
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171


class UnmountMuscaMassStorageDevice(UnmountVExpressMassStorageDevice):
    """
    Unmount Musca USB mass storage device on the dispatcher
    """

    name = "unmount-musca-usbmsd"
    description = "unmount musca usb msd"
    summary = "unmount musca usb mass storage device"

    def __init__(self):
        super().__init__()
        self.namespace_label = "musca-usb"
        self.namespace_action = "mount-musca-usbmsd"


class WaitMuscaMassStorageAction(Action):
    """
    Waits for the Musca storage device to be presented to the dispatcher.
    Often, 2 events are generated. The initial event may not have details about
    the filesystem, so if we attempt to mount at this time, the OS doesn't know
    how to mount due to lack of filesystem information.
    Therefore, ensure we listen to the second event, where the filesystem is
    known. ID_FS_VERSION=FAT16 is therefore also searched for as well as the
    disk ID.
    """

    name = "wait-musca-path"
    description = "wait for musca mass storage"
    summary = "wait for musca mass storage"

    def __init__(self, udev_action="add"):
        super().__init__()
        self.udev_action = udev_action
        # Ensure that we only trigger once FS details are known
        self.match_dict = {"ID_FS_VERSION": "FAT16"}
        self.devicepath = ""

    def validate(self):
        super().validate()
        if not isinstance(self.udev_action, str):
            self.errors = "invalid device action"
        if "board_id" not in self.job.device:
            return (
                False,
                '"board_id" is not in the device configuration (ID_SERIAL_SHORT value of serial device)',
            )
        if "id_serial" not in self.job.device["actions"]["deploy"]["methods"]["musca"]:
            return (
                False,
                '"id_serial" not set in device configuration (/dev/disk/by-id/...)',
            )
        self.devicepath = "/dev/disk/by-id/{}".format(
            self.job.device["actions"]["deploy"]["methods"]["musca"]["id_serial"]
        )

    def run(self, connection, max_end_time):
        connection = super().run(connection, max_end_time)
Dean Birch's avatar
Dean Birch committed
172
173
174
175
176
177
178
179
        if self.udev_action == "add":
            self.logger.debug("Waiting for device to appear: %s", self.devicepath)
            wait_udev_event(match_dict=self.match_dict, devicepath=self.devicepath)
        elif self.udev_action == "change":
            self.logger.debug("Waiting for device to reappear: %s", self.devicepath)
            wait_udev_changed_event(
                match_dict=self.match_dict, devicepath=self.devicepath
            )
Dean Birch's avatar
Dean Birch committed
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
        return connection


class MountMuscaMassStorageDevice(MountDeviceMassStorageDevice):
    """
    Mounts Musca mass storage device on the dispatcher.
    The device is identified by a id.
    """

    name = "mount-musca-usbmsd"
    description = "mount musca usb msd"
    summary = "mount musca usb mass storage device on the dispatcher"

    def __init__(self):
        super().__init__()
        self.disk_identifier = None
        self.disk_identifier_type = "id"
        self.namespace_label = "musca-usb"

    def validate(self):
        super().validate()
        if "id_serial" not in self.job.device["actions"]["deploy"]["methods"]["musca"]:
            self.errors = "id_serial parameter not set for actions.deploy.methods.musca"
        self.disk_identifier = self.job.device["actions"]["deploy"]["methods"]["musca"][
            "id_serial"
        ]
        if not isinstance(self.disk_identifier, str):
            self.errors = "USB ID unset for Musca"


class DeployMuscaTestBinary(Action):
    """
    Copies test binary to Musca device
    """

    name = "deploy-musca-test-binary"
    description = "deploy test binary to usb msd"
    summary = "copy test binary to Musca device"

    def __init__(self):
        super().__init__()
        self.param_key = "test_binary"

    def validate(self):
        super().validate()
        if not self.parameters["images"].get(self.param_key):
            self.errors = "Missing '%s' in 'images'" % self.param_key

    def run(self, connection, max_end_time):
        connection = super().run(connection, max_end_time)
        mount_point = self.get_namespace_data(
            action="mount-musca-usbmsd", label="musca-usb", key="mount-point"
        )
Dean Birch's avatar
Dean Birch committed
233
        if not os.path.exists(mount_point):
Dean Birch's avatar
Dean Birch committed
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
            raise InfrastructureError("Unable to locate mount point: %s" % mount_point)

        test_binary = self.get_namespace_data(
            action="download-action", label=self.param_key, key="file"
        )
        dest = os.path.join(mount_point)
        self.logger.debug("Copying %s to %s", test_binary, dest)
        shutil.copy(test_binary, dest)

        return connection


class DeployMuscaAutomationAction(Action):
    """
    Copies automation file to Musca device

    Not actually used in this flow, but allows for creation of automation files.
    https://github.com/ARMmbed/DAPLink/blob/master/docs/MSD_COMMANDS.md
    """

    name = "deploy-musca-automation-file"
    description = "deploy automation file to usb msd"
    summary = "copy automation file to Musca device"

    def __init__(self, automation_filename=""):
        super().__init__()
        self.automation_filename = automation_filename

    def validate(self):
        super().validate()
        if not self.automation_filename:
            self.errors = "Musca deploy was not given an automation filename."

    def run(self, connection, max_end_time):
        connection = super().run(connection, max_end_time)
        mount_point = self.get_namespace_data(
            action="mount-musca-usbmsd", label="musca-usb", key="mount-point"
        )
Dean Birch's avatar
Dean Birch committed
272
        if not os.path.exists(mount_point):
Dean Birch's avatar
Dean Birch committed
273
274
275
276
277
            raise InfrastructureError("Unable to locate mount point: %s" % mount_point)

        dest = os.path.join(mount_point, self.automation_filename)
        self.logger.debug("Creating empty file %s", dest)
        try:
Dean Birch's avatar
Dean Birch committed
278
279
            with open(dest, "w"):
                pass
Dean Birch's avatar
Dean Birch committed
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
        except IOError:
            raise InfrastructureError("Unable to write to %s" % dest)

        return connection


class CheckMuscaFlashAction(Action):
    """
    Checks for a FAIL.TXT file to see if there were flashing issues
    """

    name = "check-musca-flash"
    description = "checks if software flashed to the musca correctly"
    summary = "check for FAIL.TXT on musca"

    def run(self, connection, max_end_time):
        connection = super().run(connection, max_end_time)
        mount_point = self.get_namespace_data(
            action="mount-musca-usbmsd", label="musca-usb", key="mount-point"
        )
Dean Birch's avatar
Dean Birch committed
300
        if not os.path.realpath(mount_point):
Dean Birch's avatar
Dean Birch committed
301
302
303
304
305
306
307
308
309
310
311
312
313
            raise InfrastructureError("Unable to locate mount point: %s" % mount_point)

        fail_file = os.path.join(mount_point, "FAIL.TXT")
        if os.path.exists(fail_file):
            failure_details = ""
            with open(fail_file, "r") as fail_details_file:
                failure_details = fail_details_file.read().strip()
            raise InfrastructureError(
                "Flash failure indicated by presence of FAIL.TXT (Details: %s)"
                % failure_details
            )

        return connection