• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""
6Sonic host.
7
8This host can perform actions either over ssh or by submitting requests to
9an http server running on the client. Though the server provides flexibility
10and allows us to test things at a modular level, there are times we must
11resort to ssh (eg: to reboot into recovery). The server exposes the same stack
12that the chromecast extension needs to communicate with the sonic device, so
13any test involving an sonic host will fail if it cannot submit posts/gets
14to the server. In cases where we can achieve the same action over ssh or
15the rpc server, we choose the rpc server by default, because several existing
16sonic tests do the same.
17"""
18
19import logging
20import os
21
22import common
23
24from autotest_lib.client.bin import utils
25from autotest_lib.client.common_lib import autotemp
26from autotest_lib.client.common_lib import error
27from autotest_lib.server import site_utils
28from autotest_lib.server.cros import sonic_client_utils
29from autotest_lib.server.hosts import abstract_ssh
30
31
32class SonicHost(abstract_ssh.AbstractSSHHost):
33    """This class represents a sonic host."""
34
35    # Maximum time a reboot can take.
36    REBOOT_TIME = 360
37
38    COREDUMP_DIR = '/data/coredump'
39    OTA_LOCATION = '/cache/ota.zip'
40    RECOVERY_DIR = '/cache/recovery'
41    COMMAND_FILE = os.path.join(RECOVERY_DIR, 'command')
42    PLATFORM = 'sonic'
43    LABELS = [sonic_client_utils.SONIC_BOARD_LABEL]
44
45
46    @staticmethod
47    def check_host(host, timeout=10):
48        """
49        Check if the given host is a sonic host.
50
51        @param host: An ssh host representing a device.
52        @param timeout: The timeout for the run command.
53
54        @return: True if the host device is sonic.
55
56        @raises AutoservRunError: If the command failed.
57        @raises AutoservSSHTimeout: Ssh connection has timed out.
58        """
59        try:
60            result = host.run('getprop ro.product.device', timeout=timeout)
61        except (error.AutoservRunError, error.AutoservSSHTimeout,
62                error.AutotestHostRunError):
63            return False
64        return 'anchovy' in result.stdout
65
66
67    def _initialize(self, hostname, *args, **dargs):
68        super(SonicHost, self)._initialize(hostname=hostname, *args, **dargs)
69
70        # Sonic devices expose a server that can respond to json over http.
71        self.client = sonic_client_utils.SonicProxy(hostname)
72
73
74    def enable_test_extension(self):
75        """Enable a chromecast test extension on the sonic host.
76
77        Appends the extension id to the list of accepted cast
78        extensions, without which the sonic device will fail to
79        respond to any Dial requests submitted by the extension.
80
81        @raises CmdExecutionError: If the expected files are not found
82            on the sonic host.
83        """
84        extension_id = sonic_client_utils.get_extension_id()
85        tempdir = autotemp.tempdir()
86        local_dest = os.path.join(tempdir.name, 'content_shell.sh')
87        remote_src = '/system/usr/bin/content_shell.sh'
88        whitelist_flag = '--extra-cast-extension-ids'
89
90        try:
91            self.run('mount -o rw,remount /system')
92            self.get_file(remote_src, local_dest)
93            with open(local_dest) as f:
94                content = f.read()
95                if extension_id in content:
96                    return
97                if whitelist_flag in content:
98                    append_str = ',%s' % extension_id
99                else:
100                    append_str = ' %s=%s' % (whitelist_flag, extension_id)
101
102            with open(local_dest, 'a') as f:
103                f.write(append_str)
104            self.send_file(local_dest, remote_src)
105            self.reboot()
106        finally:
107            tempdir.clean()
108
109
110    def get_boot_id(self, timeout=60):
111        """Get a unique ID associated with the current boot.
112
113        @param timeout The number of seconds to wait before timing out, as
114            taken by base_utils.run.
115
116        @return A string unique to this boot or None if not available.
117        """
118        BOOT_ID_FILE = '/proc/sys/kernel/random/boot_id'
119        cmd = 'cat %r' % (BOOT_ID_FILE)
120        return self.run(cmd, timeout=timeout).stdout.strip()
121
122
123    def get_platform(self):
124        return self.PLATFORM
125
126
127    def get_labels(self):
128        return self.LABELS
129
130
131    def ssh_ping(self, timeout=60, base_cmd=''):
132        """Checks if we can ssh into the host and run getprop.
133
134        Ssh ping is vital for connectivity checks and waiting on a reboot.
135        A simple true check, or something like if [ 0 ], is not guaranteed
136        to always exit with a successful return value.
137
138        @param timeout: timeout in seconds to wait on the ssh_ping.
139        @param base_cmd: The base command to use to confirm that a round
140            trip ssh works.
141        """
142        super(SonicHost, self).ssh_ping(timeout=timeout,
143                                         base_cmd="getprop>/dev/null")
144
145
146    def verify_software(self):
147        """Verified that the server on the client device is responding to gets.
148
149        The server on the client device is crucial for the sonic device to
150        communicate with the chromecast extension. Device verify on the whole
151        consists of verify_(hardware, connectivity and software), ssh
152        connectivity is verified in the base class' verify_connectivity.
153
154        @raises: SonicProxyException if the server doesn't respond.
155        """
156        self.client.check_server()
157
158
159    def get_build_number(self, timeout_mins=1):
160        """
161        Gets the build number on the sonic device.
162
163        Since this method is usually called right after a reboot/install,
164        it has retries built in.
165
166        @param timeout_mins: The timeout in minutes.
167
168        @return: The build number of the build on the host.
169
170        @raises TimeoutError: If we're unable to get the build number within
171            the specified timeout.
172        @raises ValueError: If the build number returned isn't an integer.
173        """
174        cmd = 'getprop ro.build.version.incremental'
175        timeout = timeout_mins * 60
176        cmd_result = utils.poll_for_condition(
177                        lambda: self.run(cmd, timeout=timeout/10),
178                        timeout=timeout, sleep_interval=timeout/10)
179        return int(cmd_result.stdout)
180
181
182    def get_kernel_ver(self):
183        """Returns the build number of the build on the device."""
184        return self.get_build_number()
185
186
187    def reboot(self, timeout=5):
188        """Reboot the sonic device by submitting a post to the server."""
189
190        # TODO(beeps): crbug.com/318306
191        current_boot_id = self.get_boot_id()
192        try:
193            self.client.reboot()
194        except sonic_client_utils.SonicProxyException as e:
195            raise error.AutoservRebootError(
196                    'Unable to reboot through the sonic proxy: %s' % e)
197
198        self.wait_for_restart(timeout=timeout, old_boot_id=current_boot_id)
199
200
201    def cleanup(self):
202        """Cleanup state.
203
204        If removing state information fails, do a hard reboot. This will hit
205        our reboot method through the ssh host's cleanup.
206        """
207        try:
208            self.run('rm -r /data/*')
209            self.run('rm -f /cache/*')
210        except (error.AutotestRunError, error.AutoservRunError) as e:
211            logging.warning('Unable to remove /data and /cache %s', e)
212            super(SonicHost, self).cleanup()
213
214
215    def _remount_root(self, permissions):
216        """Remount root partition.
217
218        @param permissions: Permissions to use for the remount, eg: ro, rw.
219
220        @raises error.AutoservRunError: If something goes wrong in executing
221            the remount command.
222        """
223        self.run('mount -o %s,remount /' % permissions)
224
225
226    def _setup_coredump_dirs(self):
227        """Sets up the /data/coredump directory on the client.
228
229        The device will write a memory dump to this directory on crash,
230        if it exists. No crashdump will get written if it doesn't.
231        """
232        try:
233            self.run('mkdir -p %s' % self.COREDUMP_DIR)
234            self.run('chmod 4777 %s' % self.COREDUMP_DIR)
235        except (error.AutotestRunError, error.AutoservRunError) as e:
236            error.AutoservRunError('Unable to create coredump directories with '
237                                   'the appropriate permissions: %s' % e)
238
239
240    def _setup_for_recovery(self, update_url):
241        """Sets up the /cache/recovery directory on the client.
242
243        Copies over the OTA zipfile from the update_url to /cache, then
244        sets up the recovery directory. Normal installs are achieved
245        by rebooting into recovery mode.
246
247        @param update_url: A url pointing to a staged ota zip file.
248
249        @raises error.AutoservRunError: If something goes wrong while
250            executing a command.
251        """
252        ssh_cmd = '%s %s' % (self.make_ssh_command(), self.hostname)
253        site_utils.remote_wget(update_url, self.OTA_LOCATION, ssh_cmd)
254        self.run('ls %s' % self.OTA_LOCATION)
255
256        self.run('mkdir -p %s' % self.RECOVERY_DIR)
257
258        # These 2 commands will always return a non-zero exit status
259        # even if they complete successfully. This is a confirmed
260        # non-issue, since the install will actually complete. If one
261        # of the commands fails we can only detect it as a failure
262        # to install the specified build.
263        self.run('echo --update_package>%s' % self.COMMAND_FILE,
264                 ignore_status=True)
265        self.run('echo %s>>%s' % (self.OTA_LOCATION, self.COMMAND_FILE),
266                 ignore_status=True)
267
268
269    def machine_install(self, update_url):
270        """Installs a build on the Sonic device.
271
272        @returns String of the current build number.
273        """
274        old_build_number = self.get_build_number()
275        self._remount_root(permissions='rw')
276        self._setup_coredump_dirs()
277        self._setup_for_recovery(update_url)
278
279        current_boot_id = self.get_boot_id()
280        self.run_background('reboot recovery')
281        self.wait_for_restart(timeout=self.REBOOT_TIME,
282                              old_boot_id=current_boot_id)
283        new_build_number = self.get_build_number()
284
285        # TODO(beeps): crbug.com/318278
286        if new_build_number ==  old_build_number:
287            raise error.AutoservRunError('Build number did not change on: '
288                                         '%s after update with %s' %
289                                         (self.hostname, update_url()))
290
291        return str(new_build_number)
292