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