1# Copyright (c) 2014 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.
4import common
5import logging
6import os
7import re
8import tempfile
9import time
10import urllib2
11
12from autotest_lib.client.common_lib import error, global_config
13from autotest_lib.server.cros.dynamic_suite import frontend_wrappers
14from autotest_lib.server.hosts import cros_host
15
16
17AUTOTEST_INSTALL_DIR = global_config.global_config.get_config_value(
18        'SCHEDULER', 'drone_installation_directory')
19#'/usr/local/autotest'
20SHADOW_CONFIG_PATH = '%s/shadow_config.ini' % AUTOTEST_INSTALL_DIR
21ATEST_PATH = '%s/cli/atest' % AUTOTEST_INSTALL_DIR
22SUBNET_DUT_SEARCH_RE = (
23        r'/?.*\((?P<ip>192.168.231.*)\) at '
24        '(?P<mac>[0-9a-fA-F][0-9a-fA-F]:){5}([0-9a-fA-F][0-9a-fA-F])')
25MOBLAB_IMAGE_STORAGE = '/mnt/moblab/static'
26MOBLAB_BOTO_LOCATION = '/home/moblab/.boto'
27MOBLAB_LAUNCH_CONTROL_KEY_LOCATION = '/home/moblab/.launch_control_key'
28MOBLAB_AUTODIR = '/usr/local/autodir'
29DHCPD_LEASE_FILE = '/var/lib/dhcp/dhcpd.leases'
30MOBLAB_SERVICES = ['moblab-scheduler-init',
31                   'moblab-database-init',
32                   'moblab-devserver-init',
33                   'moblab-gsoffloader-init',
34                   'moblab-gsoffloader_s-init']
35MOBLAB_PROCESSES = ['apache2', 'dhcpd']
36DUT_VERIFY_SLEEP_SECS = 5
37DUT_VERIFY_TIMEOUT = 15 * 60
38MOBLAB_TMP_DIR = '/mnt/moblab/tmp'
39
40
41class MoblabHost(cros_host.CrosHost):
42    """Moblab specific host class."""
43
44
45    def _initialize(self, *args, **dargs):
46        super(MoblabHost, self)._initialize(*args, **dargs)
47        # Clear the Moblab Image Storage so that staging an image is properly
48        # tested.
49        if dargs.get('retain_image_storage') is not True:
50            self.run('rm -rf %s/*' % MOBLAB_IMAGE_STORAGE)
51        self._dhcpd_leasefile = None
52        self.web_address = dargs.get('web_address', self.hostname)
53        timeout_min = dargs.get('rpc_timeout_min', 1)
54        self.afe = frontend_wrappers.RetryingAFE(timeout_min=timeout_min,
55                                                 user='moblab',
56                                                 server=self.web_address)
57        self.tko = frontend_wrappers.RetryingTKO(timeout_min=timeout_min,
58                                                 user='moblab',
59                                                 server=self.web_address)
60
61
62    @staticmethod
63    def check_host(host, timeout=10):
64        """
65        Check if the given host is an moblab host.
66
67        @param host: An ssh host representing a device.
68        @param timeout: The timeout for the run command.
69
70
71        @return: True if the host device has adb.
72
73        @raises AutoservRunError: If the command failed.
74        @raises AutoservSSHTimeout: Ssh connection has timed out.
75        """
76        try:
77            result = host.run(
78                    'grep -q moblab /etc/lsb-release && '
79                    '! test -f /mnt/stateful_partition/.android_tester',
80                    ignore_status=True, timeout=timeout)
81        except (error.AutoservRunError, error.AutoservSSHTimeout):
82            return False
83        return result.exit_status == 0
84
85
86    def install_boto_file(self, boto_path=''):
87        """Install a boto file on the Moblab device.
88
89        @param boto_path: Path to the boto file to install. If None, sends the
90                          boto file in the current HOME directory.
91
92        @raises error.TestError if the boto file does not exist.
93        """
94        if not boto_path:
95            boto_path = os.path.join(os.getenv('HOME'), '.boto')
96        if not os.path.exists(boto_path):
97            raise error.TestError('Boto File:%s does not exist.' % boto_path)
98        self.send_file(boto_path, MOBLAB_BOTO_LOCATION)
99        self.run('chown moblab:moblab %s' % MOBLAB_BOTO_LOCATION)
100
101
102    def get_autodir(self):
103        """Return the directory to install autotest for client side tests."""
104        return self.autodir or MOBLAB_AUTODIR
105
106
107    def run_as_moblab(self, command, **kwargs):
108        """Moblab commands should be ran as the moblab user not root.
109
110        @param command: Command to run as user moblab.
111        """
112        command = "su - moblab -c '%s'" % command
113        return self.run(command, **kwargs)
114
115
116    def reboot(self, **dargs):
117        """Reboot the Moblab Host and wait for its services to restart."""
118        super(MoblabHost, self).reboot(**dargs)
119        # In general after a reboot, we want to wait till the web frontend
120        # and other Autotest services are up before executing. However should
121        # something be wrong with these services, repair needs to be able
122        # to continue and reimage the device.
123        try:
124            self.wait_afe_up()
125        except (urllib2.HTTPError, urllib2.URLError) as e:
126            logging.error('DUT has rebooted but AFE has failed to load.: %s',
127                          e)
128
129
130    def wait_afe_up(self, timeout_min=5):
131        """Wait till the AFE is up and loaded.
132
133        Attempt to reach the Moblab's AFE and database through its RPC
134        interface.
135
136        @param timeout_min: Minutes to wait for the AFE to respond. Default is
137                            5 minutes.
138
139        @raises urllib2.HTTPError if AFE does not respond within the timeout.
140        """
141        # Use a new AFE object with a longer timeout to wait for the AFE to
142        # load.
143        afe = frontend_wrappers.RetryingAFE(timeout_min=timeout_min,
144                                            server=self.hostname)
145        # Verify the AFE can handle a simple request.
146        afe.get_hosts()
147
148
149    def _wake_devices(self):
150        """Search the subnet and attempt to ping any available duts.
151
152        Fills up the arp table with entries about devices on the subnet.
153
154        Either uses fping or directly pings devices listed in the dhcpd lease
155        file.
156        """
157        fping_result = self.run('fping -g 192.168.231.100 192.168.231.120',
158                                ignore_status=True)
159        # If fping is not on the system, ping entries in the dhcpd lease file.
160        if fping_result.exit_status == 127:
161            leases = set(self.run('grep ^lease %s' % DHCPD_LEASE_FILE,
162                                  ignore_status=True).stdout.splitlines())
163            for lease in leases:
164                ip = re.match('lease (?P<ip>.*) {', lease).groups('ip')
165                self.run('ping %s -w 1' % ip, ignore_status=True)
166
167
168    def add_dut(self, hostname):
169        """Add a DUT hostname to the AFE.
170
171        @param hostname: DUT hostname to add.
172        """
173        result = self.run_as_moblab('%s host create %s' % (ATEST_PATH,
174                                                           hostname))
175        logging.debug('atest host create output for host %s:\n%s',
176                      hostname, result.stdout)
177
178
179    def find_and_add_duts(self):
180        """Discover DUTs on the testing subnet and add them to the AFE.
181
182        Runs 'arp -a' on the Moblab host and parses the output to discover DUTs
183        and if they are not already in the AFE, adds them.
184        """
185        self._wake_devices()
186        existing_hosts = [host.hostname for host in self.afe.get_hosts()]
187        arp_command = self.run('arp -a')
188        for line in arp_command.stdout.splitlines():
189            match = re.match(SUBNET_DUT_SEARCH_RE, line)
190            if match:
191                dut_hostname = match.group('ip')
192                if dut_hostname in existing_hosts:
193                    break
194                self.add_dut(dut_hostname)
195
196
197    def verify_software(self):
198        """Verify working software on a Chrome OS system.
199
200        Tests for the following conditions:
201         1. All conditions tested by the parent version of this
202            function.
203         2. Ensures that Moblab services are running.
204         3. Ensures that both DUTs successfully run Verify.
205
206        """
207        # In case cleanup or powerwash wiped the autodir, create an empty
208        # directory.
209        self.run('mkdir -p %s' % MOBLAB_AUTODIR)
210        super(MoblabHost, self).verify_software()
211        self._verify_moblab_services()
212        self._verify_duts()
213
214
215    def _verify_moblab_services(self):
216        """Verify the required Moblab services are up and running.
217
218        @raises AutoservError if any moblab service is not running.
219        """
220        for service in MOBLAB_SERVICES:
221            if not self.upstart_status(service):
222                raise error.AutoservError('Moblab service: %s is not running.'
223                                          % service)
224        for process in MOBLAB_PROCESSES:
225            try:
226                self.run('pgrep %s' % process)
227            except error.AutoservRunError:
228                raise error.AutoservError('Moblab process: %s is not running.'
229                                          % process)
230
231
232    def _verify_duts(self):
233        """Verify the Moblab DUTs are up and running.
234
235        @raises AutoservError if no DUTs are in the Ready State.
236        """
237        # Add the DUTs if they have not yet been added.
238        self.find_and_add_duts()
239        # Ensure a boto file is installed in case this Moblab was wiped in
240        # repair.
241        self.install_boto_file()
242        hosts = self.afe.reverify_hosts()
243        logging.debug('DUTs scheduled for reverification: %s', hosts)
244        # Wait till all pending special tasks are completed.
245        total_time = 0
246        while (self.afe.get_special_tasks(is_complete=False) and
247               total_time < DUT_VERIFY_TIMEOUT):
248            total_time = total_time + DUT_VERIFY_SLEEP_SECS
249            time.sleep(DUT_VERIFY_SLEEP_SECS)
250        if not self.afe.get_hosts(status='Ready'):
251            for host in self.afe.get_hosts():
252                logging.error('DUT: %s Status: %s', host, host.status)
253            raise error.AutoservError('Moblab has 0 Ready DUTs')
254
255
256    def check_device(self):
257        """Moblab specific check_device.
258
259        Runs after a repair method has been attempted:
260        * Reboots the moblab to start its services.
261        * Creates the autotest client directory in case powerwash was used to
262          wipe stateful and repair.
263        * Reinstall the dhcp lease file if it was preserved.
264        """
265        # Moblab requires a reboot to initialize it's services prior to
266        # verification.
267        self.reboot()
268        self.wait_afe_up()
269        # Stateful could have been wiped so setup an empty autotest client
270        # directory.
271        self.run('mkdir -p %s' % self.get_autodir(), ignore_status=True)
272        # Restore the dhcpd lease file if it was backed up.
273        # TODO (sbasi) - Currently this is required for repairs but may need
274        # to be expanded to regular installs as well.
275        if self._dhcpd_leasefile:
276            self.send_file(self._dhcpd_leasefile.name, DHCPD_LEASE_FILE)
277            self.run('chown dhcp:dhcp %s' % DHCPD_LEASE_FILE)
278        super(MoblabHost, self).check_device()
279
280
281    def repair(self):
282        """Moblab specific repair.
283
284        Preserves the dhcp lease file prior to repairing the device.
285        """
286        try:
287            temp = tempfile.TemporaryFile()
288            self.get_file(DHCPD_LEASE_FILE, temp.name)
289            self._dhcpd_leasefile = temp
290        except error.AutoservRunError:
291            logging.debug('Failed to retrieve dhcpd lease file from host.')
292        super(MoblabHost, self).repair()
293
294
295    def get_platform(self):
296        """Determine the correct platform label for this host.
297
298        For Moblab devices '_moblab' is appended.
299
300        @returns a string representing this host's platform.
301        """
302        return super(MoblabHost, self).get_platform() + '_moblab'
303
304
305    def make_tmp_dir(self, base=MOBLAB_TMP_DIR):
306        """Creates a temporary directory.
307
308        @param base: The directory where it should be created.
309
310        @return Path to a newly created temporary directory.
311        """
312        self.run('mkdir -p %s' % base)
313        return self.run('mktemp -d -p %s' % base).stdout.strip()
314