xref: /aosp_15_r20/external/autotest/server/hosts/remote.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li# Lint as: python2, python3
2*9c5db199SXin Li"""This class defines the Remote host class."""
3*9c5db199SXin Li
4*9c5db199SXin Lifrom __future__ import absolute_import
5*9c5db199SXin Lifrom __future__ import division
6*9c5db199SXin Lifrom __future__ import print_function
7*9c5db199SXin Liimport os, logging, time
8*9c5db199SXin Liimport six
9*9c5db199SXin Lifrom six.moves import urllib
10*9c5db199SXin Liimport re
11*9c5db199SXin Li
12*9c5db199SXin Liimport common
13*9c5db199SXin Li
14*9c5db199SXin Lifrom autotest_lib.client.common_lib import error
15*9c5db199SXin Lifrom autotest_lib.client.common_lib.global_config import global_config
16*9c5db199SXin Lifrom autotest_lib.server import utils
17*9c5db199SXin Lifrom autotest_lib.server.hosts import base_classes
18*9c5db199SXin Lifrom autotest_lib.server.hosts.tls_client.connection import TLSConnection
19*9c5db199SXin Li
20*9c5db199SXin Li
21*9c5db199SXin Liclass RemoteHost(base_classes.Host):
22*9c5db199SXin Li    """
23*9c5db199SXin Li    This class represents a remote machine on which you can run
24*9c5db199SXin Li    programs.
25*9c5db199SXin Li
26*9c5db199SXin Li    It may be accessed through a network, a serial line, ...
27*9c5db199SXin Li    It is not the machine autoserv is running on.
28*9c5db199SXin Li
29*9c5db199SXin Li    Implementation details:
30*9c5db199SXin Li    This is an abstract class, leaf subclasses must implement the methods
31*9c5db199SXin Li    listed here and in parent classes which have no implementation. They
32*9c5db199SXin Li    may reimplement methods which already have an implementation. You
33*9c5db199SXin Li    must not instantiate this class but should instantiate one of those
34*9c5db199SXin Li    leaf subclasses.
35*9c5db199SXin Li    """
36*9c5db199SXin Li
37*9c5db199SXin Li    DEFAULT_REBOOT_TIMEOUT = base_classes.Host.DEFAULT_REBOOT_TIMEOUT
38*9c5db199SXin Li    DEFAULT_HALT_TIMEOUT = 2 * 60
39*9c5db199SXin Li    _LABEL_FUNCTIONS = []
40*9c5db199SXin Li    _DETECTABLE_LABELS = []
41*9c5db199SXin Li
42*9c5db199SXin Li    VAR_LOG_MESSAGES_COPY_PATH = "/var/tmp/messages.autotest_start"
43*9c5db199SXin Li    TMP_DIR_TEMPLATE = '/usr/local/tmp/autoserv-XXXXXX'
44*9c5db199SXin Li
45*9c5db199SXin Li
46*9c5db199SXin Li    def _initialize(self, hostname, autodir=None, *args, **dargs):
47*9c5db199SXin Li        super(RemoteHost, self)._initialize(*args, **dargs)
48*9c5db199SXin Li
49*9c5db199SXin Li        self.hostname = hostname
50*9c5db199SXin Li        self.autodir = autodir
51*9c5db199SXin Li        self.tmp_dirs = []
52*9c5db199SXin Li
53*9c5db199SXin Li        get_value = global_config.get_config_value
54*9c5db199SXin Li
55*9c5db199SXin Li        self.tls_connection = None
56*9c5db199SXin Li        try:
57*9c5db199SXin Li            self.tls_connection = TLSConnection()
58*9c5db199SXin Li        except Exception as e:
59*9c5db199SXin Li            logging.warning("Could not establish TLS connection %s", e)
60*9c5db199SXin Li
61*9c5db199SXin Li    def __repr__(self):
62*9c5db199SXin Li        return "<remote host: %s>" % self.hostname
63*9c5db199SXin Li
64*9c5db199SXin Li
65*9c5db199SXin Li    def close(self):
66*9c5db199SXin Li        # pylint: disable=missing-docstring
67*9c5db199SXin Li        super(RemoteHost, self).close()
68*9c5db199SXin Li        self.stop_loggers()
69*9c5db199SXin Li
70*9c5db199SXin Li        if hasattr(self, 'tmp_dirs'):
71*9c5db199SXin Li            for dir in self.tmp_dirs:
72*9c5db199SXin Li                try:
73*9c5db199SXin Li                    self.run('rm -rf "%s"' % (utils.sh_escape(dir)))
74*9c5db199SXin Li                except error.AutoservRunError:
75*9c5db199SXin Li                    pass
76*9c5db199SXin Li        if self.tls_connection:
77*9c5db199SXin Li            self.tls_connection.close()
78*9c5db199SXin Li            self.tls_connection = None
79*9c5db199SXin Li
80*9c5db199SXin Li    def job_start(self):
81*9c5db199SXin Li        """
82*9c5db199SXin Li        Abstract method, called the first time a remote host object
83*9c5db199SXin Li        is created for a specific host after a job starts.
84*9c5db199SXin Li
85*9c5db199SXin Li        This method depends on the create_host factory being used to
86*9c5db199SXin Li        construct your host object. If you directly construct host objects
87*9c5db199SXin Li        you will need to call this method yourself (and enforce the
88*9c5db199SXin Li        single-call rule).
89*9c5db199SXin Li        """
90*9c5db199SXin Li        try:
91*9c5db199SXin Li            cmd = ('test ! -e /var/log/messages || cp -f /var/log/messages '
92*9c5db199SXin Li                   '%s') % self.VAR_LOG_MESSAGES_COPY_PATH
93*9c5db199SXin Li            self.run(cmd)
94*9c5db199SXin Li        except Exception as e:
95*9c5db199SXin Li            # Non-fatal error
96*9c5db199SXin Li            logging.info('Failed to copy /var/log/messages at startup: %s', e)
97*9c5db199SXin Li
98*9c5db199SXin Li
99*9c5db199SXin Li    def get_autodir(self):
100*9c5db199SXin Li        return self.autodir
101*9c5db199SXin Li
102*9c5db199SXin Li
103*9c5db199SXin Li    def set_autodir(self, autodir):
104*9c5db199SXin Li        """
105*9c5db199SXin Li        This method is called to make the host object aware of the
106*9c5db199SXin Li        where autotest is installed. Called in server/autotest.py
107*9c5db199SXin Li        after a successful install
108*9c5db199SXin Li        """
109*9c5db199SXin Li        self.autodir = autodir
110*9c5db199SXin Li
111*9c5db199SXin Li
112*9c5db199SXin Li    def sysrq_reboot(self):
113*9c5db199SXin Li        # pylint: disable=missing-docstring
114*9c5db199SXin Li        self.run_background('echo b > /proc/sysrq-trigger')
115*9c5db199SXin Li
116*9c5db199SXin Li
117*9c5db199SXin Li    def halt(self, timeout=DEFAULT_HALT_TIMEOUT, wait=True):
118*9c5db199SXin Li        """
119*9c5db199SXin Li        Shut down the remote host.
120*9c5db199SXin Li
121*9c5db199SXin Li        N.B.  This method makes no provision to bring the target back
122*9c5db199SXin Li        up.  The target will be offline indefinitely if there's no
123*9c5db199SXin Li        independent hardware (servo, RPM, etc.) to force the target to
124*9c5db199SXin Li        power on.
125*9c5db199SXin Li
126*9c5db199SXin Li        @param timeout  Maximum time to wait for host down, in seconds.
127*9c5db199SXin Li        @param wait  Whether to wait for the host to go offline.
128*9c5db199SXin Li        """
129*9c5db199SXin Li        self.run_background('sleep 1 ; halt')
130*9c5db199SXin Li        if wait:
131*9c5db199SXin Li            self.wait_down(timeout=timeout)
132*9c5db199SXin Li
133*9c5db199SXin Li
134*9c5db199SXin Li    def reboot(self, timeout=DEFAULT_REBOOT_TIMEOUT, wait=True,
135*9c5db199SXin Li               fastsync=False, reboot_cmd=None, **dargs):
136*9c5db199SXin Li        """
137*9c5db199SXin Li        Reboot the remote host.
138*9c5db199SXin Li
139*9c5db199SXin Li        Args:
140*9c5db199SXin Li                timeout - How long to wait for the reboot.
141*9c5db199SXin Li                wait - Should we wait to see if the machine comes back up.
142*9c5db199SXin Li                       If this is set to True, ignores reboot_cmd's error
143*9c5db199SXin Li                       even if occurs.
144*9c5db199SXin Li                fastsync - Don't wait for the sync to complete, just start one
145*9c5db199SXin Li                        and move on. This is for cases where rebooting prompty
146*9c5db199SXin Li                        is more important than data integrity and/or the
147*9c5db199SXin Li                        machine may have disks that cause sync to never return.
148*9c5db199SXin Li                reboot_cmd - Reboot command to execute.
149*9c5db199SXin Li        """
150*9c5db199SXin Li        self.reboot_setup(**dargs)
151*9c5db199SXin Li        if not reboot_cmd:
152*9c5db199SXin Li            reboot_cmd = ('sync & sleep 5; '
153*9c5db199SXin Li                          'reboot & sleep 60; '
154*9c5db199SXin Li                          'reboot -f & sleep 10; '
155*9c5db199SXin Li                          'reboot -nf & sleep 10; '
156*9c5db199SXin Li                          'telinit 6')
157*9c5db199SXin Li
158*9c5db199SXin Li        def reboot():
159*9c5db199SXin Li            # pylint: disable=missing-docstring
160*9c5db199SXin Li            self.record("GOOD", None, "reboot.start")
161*9c5db199SXin Li            current_boot_id = None
162*9c5db199SXin Li            try:
163*9c5db199SXin Li                current_boot_id = self.get_boot_id()
164*9c5db199SXin Li
165*9c5db199SXin Li                # sync before starting the reboot, so that a long sync during
166*9c5db199SXin Li                # shutdown isn't timed out by wait_down's short timeout
167*9c5db199SXin Li                if not fastsync:
168*9c5db199SXin Li                    self.run('sync; sync', timeout=timeout, ignore_status=True)
169*9c5db199SXin Li
170*9c5db199SXin Li                self.run_background(reboot_cmd)
171*9c5db199SXin Li            except error.AutoservRunError:
172*9c5db199SXin Li                # If wait is set, ignore the error here, and rely on the
173*9c5db199SXin Li                # wait_for_restart() for stability, instead.
174*9c5db199SXin Li                # reboot_cmd sometimes causes an error even if reboot is
175*9c5db199SXin Li                # successfully in progress. This is difficult to be avoided,
176*9c5db199SXin Li                # because we have no much control on remote machine after
177*9c5db199SXin Li                # "reboot" starts.
178*9c5db199SXin Li                if not wait or current_boot_id is None:
179*9c5db199SXin Li                    # TODO(b/37652392): Revisit no-wait case, later.
180*9c5db199SXin Li                    self.record("ABORT", None, "reboot.start",
181*9c5db199SXin Li                                "reboot command failed")
182*9c5db199SXin Li                    raise
183*9c5db199SXin Li            if wait:
184*9c5db199SXin Li                self.wait_for_restart(timeout, old_boot_id=current_boot_id,
185*9c5db199SXin Li                                      **dargs)
186*9c5db199SXin Li
187*9c5db199SXin Li        # if this is a full reboot-and-wait, run the reboot inside a group
188*9c5db199SXin Li        if wait:
189*9c5db199SXin Li            self.log_op(self.OP_REBOOT, reboot)
190*9c5db199SXin Li        else:
191*9c5db199SXin Li            reboot()
192*9c5db199SXin Li
193*9c5db199SXin Li    def suspend(self, timeout, suspend_cmd,
194*9c5db199SXin Li                allow_early_resume=False):
195*9c5db199SXin Li        """
196*9c5db199SXin Li        Suspend the remote host.
197*9c5db199SXin Li
198*9c5db199SXin Li        Args:
199*9c5db199SXin Li                timeout - How long to wait for the suspend in integer seconds.
200*9c5db199SXin Li                suspend_cmd - suspend command to execute.
201*9c5db199SXin Li                allow_early_resume - Boolean that indicate whether resume
202*9c5db199SXin Li                                     before |timeout| is ok.
203*9c5db199SXin Li        Raises:
204*9c5db199SXin Li                error.AutoservSuspendError - If |allow_early_resume| is False
205*9c5db199SXin Li                                             and if device resumes before
206*9c5db199SXin Li                                             |timeout|.
207*9c5db199SXin Li        """
208*9c5db199SXin Li        # define a function for the supend and run it in a group
209*9c5db199SXin Li        def suspend():
210*9c5db199SXin Li            # pylint: disable=missing-docstring
211*9c5db199SXin Li            self.record("GOOD", None, "suspend.start for %d seconds" % (timeout))
212*9c5db199SXin Li            try:
213*9c5db199SXin Li                self.run_background(suspend_cmd)
214*9c5db199SXin Li            except error.AutoservRunError:
215*9c5db199SXin Li                self.record("ABORT", None, "suspend.start",
216*9c5db199SXin Li                            "suspend command failed")
217*9c5db199SXin Li                raise error.AutoservSuspendError("suspend command failed")
218*9c5db199SXin Li
219*9c5db199SXin Li            # Wait for some time, to ensure the machine is going to sleep.
220*9c5db199SXin Li            # Not too long to check if the machine really suspended.
221*9c5db199SXin Li            time_slice = min(timeout / 2, 300)
222*9c5db199SXin Li            time.sleep(time_slice)
223*9c5db199SXin Li            time_counter = time_slice
224*9c5db199SXin Li            while time_counter < timeout + 60:
225*9c5db199SXin Li                # Check if the machine is back. We check regularely to
226*9c5db199SXin Li                # ensure the machine was suspended long enough.
227*9c5db199SXin Li                if utils.ping(self.hostname, tries=1, deadline=1) == 0:
228*9c5db199SXin Li                    return
229*9c5db199SXin Li                else:
230*9c5db199SXin Li                    if time_counter > timeout - 10:
231*9c5db199SXin Li                        time_slice = 5
232*9c5db199SXin Li                    time.sleep(time_slice)
233*9c5db199SXin Li                    time_counter += time_slice
234*9c5db199SXin Li
235*9c5db199SXin Li            if utils.ping(self.hostname, tries=1, deadline=1) != 0:
236*9c5db199SXin Li                raise error.AutoservSuspendError(
237*9c5db199SXin Li                    "DUT is not responding after %d seconds" % (time_counter))
238*9c5db199SXin Li
239*9c5db199SXin Li        start_time = time.time()
240*9c5db199SXin Li        self.log_op(self.OP_SUSPEND, suspend)
241*9c5db199SXin Li        lasted = time.time() - start_time
242*9c5db199SXin Li        logging.info("Device resumed after %d secs", lasted)
243*9c5db199SXin Li        if (lasted < timeout and not allow_early_resume):
244*9c5db199SXin Li            raise error.AutoservSuspendError(
245*9c5db199SXin Li                "Suspend did not last long enough: %d instead of %d" % (
246*9c5db199SXin Li                    lasted, timeout))
247*9c5db199SXin Li
248*9c5db199SXin Li    def reboot_followup(self, *args, **dargs):
249*9c5db199SXin Li        # pylint: disable=missing-docstring
250*9c5db199SXin Li        super(RemoteHost, self).reboot_followup(*args, **dargs)
251*9c5db199SXin Li        if self.job:
252*9c5db199SXin Li            self.job.profilers.handle_reboot(self)
253*9c5db199SXin Li
254*9c5db199SXin Li
255*9c5db199SXin Li    def wait_for_restart(self, timeout=DEFAULT_REBOOT_TIMEOUT, **dargs):
256*9c5db199SXin Li        """
257*9c5db199SXin Li        Wait for the host to come back from a reboot. This wraps the
258*9c5db199SXin Li        generic wait_for_restart implementation in a reboot group.
259*9c5db199SXin Li        """
260*9c5db199SXin Li        def op_func():
261*9c5db199SXin Li            # pylint: disable=missing-docstring
262*9c5db199SXin Li            super(RemoteHost, self).wait_for_restart(timeout=timeout, **dargs)
263*9c5db199SXin Li        self.log_op(self.OP_REBOOT, op_func)
264*9c5db199SXin Li
265*9c5db199SXin Li
266*9c5db199SXin Li    def cleanup(self):
267*9c5db199SXin Li        # pylint: disable=missing-docstring
268*9c5db199SXin Li        super(RemoteHost, self).cleanup()
269*9c5db199SXin Li        self.reboot()
270*9c5db199SXin Li
271*9c5db199SXin Li
272*9c5db199SXin Li    def get_tmp_dir(self, parent='/tmp'):
273*9c5db199SXin Li        """
274*9c5db199SXin Li        Return the pathname of a directory on the host suitable
275*9c5db199SXin Li        for temporary file storage.
276*9c5db199SXin Li
277*9c5db199SXin Li        The directory and its content will be deleted automatically
278*9c5db199SXin Li        on the destruction of the Host object that was used to obtain
279*9c5db199SXin Li        it.
280*9c5db199SXin Li        """
281*9c5db199SXin Li        template = os.path.join(parent, self.TMP_DIR_TEMPLATE)
282*9c5db199SXin Li        parent = os.path.dirname(template)
283*9c5db199SXin Li        dir_name = self.run('mkdir -p %s && mktemp -d %s' % (parent, template)).stdout.rstrip()
284*9c5db199SXin Li        self.tmp_dirs.append(dir_name)
285*9c5db199SXin Li        return dir_name
286*9c5db199SXin Li
287*9c5db199SXin Li
288*9c5db199SXin Li    def get_platform_label(self):
289*9c5db199SXin Li        """
290*9c5db199SXin Li        Return the platform label, or None if platform label is not set.
291*9c5db199SXin Li        """
292*9c5db199SXin Li
293*9c5db199SXin Li        if self.job:
294*9c5db199SXin Li            keyval_path = os.path.join(self.job.resultdir, 'host_keyvals',
295*9c5db199SXin Li                                       self.hostname)
296*9c5db199SXin Li            keyvals = utils.read_keyval(keyval_path)
297*9c5db199SXin Li            return keyvals.get('platform', None)
298*9c5db199SXin Li        else:
299*9c5db199SXin Li            return None
300*9c5db199SXin Li
301*9c5db199SXin Li
302*9c5db199SXin Li    def get_all_labels(self):
303*9c5db199SXin Li        """
304*9c5db199SXin Li        Return all labels, or empty list if label is not set.
305*9c5db199SXin Li        """
306*9c5db199SXin Li        if self.job:
307*9c5db199SXin Li            keyval_path = os.path.join(self.job.resultdir, 'host_keyvals',
308*9c5db199SXin Li                                       self.hostname)
309*9c5db199SXin Li            keyvals = utils.read_keyval(keyval_path)
310*9c5db199SXin Li            all_labels = keyvals.get('labels', '')
311*9c5db199SXin Li            if all_labels:
312*9c5db199SXin Li                all_labels = all_labels.split(',')
313*9c5db199SXin Li                return [urllib.parse.unquote(label) for label in all_labels]
314*9c5db199SXin Li        return []
315*9c5db199SXin Li
316*9c5db199SXin Li
317*9c5db199SXin Li    def delete_tmp_dir(self, tmpdir):
318*9c5db199SXin Li        """
319*9c5db199SXin Li        Delete the given temporary directory on the remote machine.
320*9c5db199SXin Li
321*9c5db199SXin Li        @param tmpdir The directory to delete.
322*9c5db199SXin Li        """
323*9c5db199SXin Li        self.run('rm -rf "%s"' % utils.sh_escape(tmpdir), ignore_status=True)
324*9c5db199SXin Li        self.tmp_dirs.remove(tmpdir)
325*9c5db199SXin Li
326*9c5db199SXin Li
327*9c5db199SXin Li    def delete_all_tmp_dirs(self, parent='/tmp'):
328*9c5db199SXin Li        """
329*9c5db199SXin Li        Delete all directories in parent that were created by get_tmp_dir
330*9c5db199SXin Li
331*9c5db199SXin Li        Note that this may involve deleting directories created by calls to
332*9c5db199SXin Li        get_tmp_dir on a different RemoteHost instance than the one running this
333*9c5db199SXin Li        method. Only perform this operation when certain that this will not
334*9c5db199SXin Li        cause unexpected behavior.
335*9c5db199SXin Li        """
336*9c5db199SXin Li        # follow mktemp's behavior of only expanding 3 or more consecutive Xs
337*9c5db199SXin Li        if isinstance(parent, (list, tuple)):
338*9c5db199SXin Li            parents = parent
339*9c5db199SXin Li        else:
340*9c5db199SXin Li            parents = [parent]
341*9c5db199SXin Li        rm_paths = []
342*9c5db199SXin Li        for parent in parents:
343*9c5db199SXin Li            base_template = re.sub('XXXX*', '*', self.TMP_DIR_TEMPLATE)
344*9c5db199SXin Li            # distinguish between non-wildcard asterisks in parent directory name
345*9c5db199SXin Li            # and wildcards inserted from the template
346*9c5db199SXin Li            base = '*'.join(
347*9c5db199SXin Li                ['"%s"' % utils.sh_escape(x) for x in base_template.split('*')])
348*9c5db199SXin Li            path = '"%s' % os.path.join(utils.sh_escape(parent), base[1:])
349*9c5db199SXin Li            rm_paths.append(path)
350*9c5db199SXin Li            # remove deleted directories from tmp_dirs
351*9c5db199SXin Li            regex = os.path.join(parent, re.sub('(XXXX*)',
352*9c5db199SXin Li                            lambda match: '[a-zA-Z0-9]{%d}' % len(match.group(1)),
353*9c5db199SXin Li                            self.TMP_DIR_TEMPLATE))
354*9c5db199SXin Li            regex += '(/|$)' # remove if matches, or is within a dir that matches
355*9c5db199SXin Li            self.tmp_dirs = [x for x in self.tmp_dirs if not re.match(regex, x)]
356*9c5db199SXin Li
357*9c5db199SXin Li        self.run('rm -rf {}'.format(" ".join(rm_paths)), ignore_status=True)
358*9c5db199SXin Li
359*9c5db199SXin Li    def check_uptime(self):
360*9c5db199SXin Li        """
361*9c5db199SXin Li        Check that uptime is available and monotonically increasing.
362*9c5db199SXin Li        """
363*9c5db199SXin Li        if not self.is_up():
364*9c5db199SXin Li            raise error.AutoservHostError('Client does not appear to be up')
365*9c5db199SXin Li        result = self.run("/bin/cat /proc/uptime", 30)
366*9c5db199SXin Li        return result.stdout.strip().split()[0]
367*9c5db199SXin Li
368*9c5db199SXin Li
369*9c5db199SXin Li    def check_for_lkdtm(self):
370*9c5db199SXin Li        """
371*9c5db199SXin Li        Check for kernel dump test module. return True if exist.
372*9c5db199SXin Li        """
373*9c5db199SXin Li        cmd = 'ls /sys/kernel/debug/provoke-crash/DIRECT'
374*9c5db199SXin Li        return self.run(cmd, ignore_status=True).exit_status == 0
375*9c5db199SXin Li
376*9c5db199SXin Li
377*9c5db199SXin Li    def are_wait_up_processes_up(self):
378*9c5db199SXin Li        """
379*9c5db199SXin Li        Checks if any HOSTS waitup processes are running yet on the
380*9c5db199SXin Li        remote host.
381*9c5db199SXin Li
382*9c5db199SXin Li        Returns True if any the waitup processes are running, False
383*9c5db199SXin Li        otherwise.
384*9c5db199SXin Li        """
385*9c5db199SXin Li        processes = self.get_wait_up_processes()
386*9c5db199SXin Li        if len(processes) == 0:
387*9c5db199SXin Li            return True # wait up processes aren't being used
388*9c5db199SXin Li        for procname in processes:
389*9c5db199SXin Li            exit_status = self.run("{ ps -e || ps; } | grep '%s'" % procname,
390*9c5db199SXin Li                                   ignore_status=True).exit_status
391*9c5db199SXin Li            if exit_status == 0:
392*9c5db199SXin Li                return True
393*9c5db199SXin Li        return False
394*9c5db199SXin Li
395*9c5db199SXin Li
396*9c5db199SXin Li    def get_labels(self):
397*9c5db199SXin Li        """Return a list of labels for this given host.
398*9c5db199SXin Li
399*9c5db199SXin Li        This is the main way to retrieve all the automatic labels for a host
400*9c5db199SXin Li        as it will run through all the currently implemented label functions.
401*9c5db199SXin Li        """
402*9c5db199SXin Li        labels = []
403*9c5db199SXin Li        for label_function in self._LABEL_FUNCTIONS:
404*9c5db199SXin Li            try:
405*9c5db199SXin Li                label = label_function(self)
406*9c5db199SXin Li            except Exception:
407*9c5db199SXin Li                logging.exception('Label function %s failed; ignoring it.',
408*9c5db199SXin Li                                  label_function.__name__)
409*9c5db199SXin Li                label = None
410*9c5db199SXin Li            if label:
411*9c5db199SXin Li                if type(label) is str:
412*9c5db199SXin Li                    labels.append(label)
413*9c5db199SXin Li                elif type(label) is list:
414*9c5db199SXin Li                    labels.extend(label)
415*9c5db199SXin Li        return labels
416*9c5db199SXin Li
417*9c5db199SXin Li    def get_result_dir(self):
418*9c5db199SXin Li        """Return the result directory path if passed or None if not.
419*9c5db199SXin Li
420*9c5db199SXin Li        @return string
421*9c5db199SXin Li        """
422*9c5db199SXin Li        if self.job and hasattr(self.job, 'resultdir'):
423*9c5db199SXin Li            return self.job.resultdir
424*9c5db199SXin Li        return None
425