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