1# Copyright (c) 2012 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
5import collections
6import json
7import logging
8import os
9import socket
10import time
11import urllib2
12import urlparse
13
14from autotest_lib.client.bin import utils as client_utils
15from autotest_lib.client.common_lib import error, global_config
16from autotest_lib.client.common_lib.cros import autoupdater, dev_server
17from autotest_lib.server import afe_utils, autotest, hosts, test
18from autotest_lib.server.cros.dynamic_suite import tools
19
20
21def _wait(secs, desc=None):
22    """Emits a log message and sleeps for a given number of seconds."""
23    msg = 'Waiting %s seconds' % secs
24    if desc:
25        msg += ' (%s)' % desc
26    logging.info(msg)
27    time.sleep(secs)
28
29
30class ExpectedUpdateEventChainFailed(error.TestFail):
31    """Raised if we fail to receive an expected event in a chain."""
32
33class RequiredArgumentMissing(error.TestError):
34    """Raised if the test is missing a required argument."""
35
36
37# Update event types.
38EVENT_TYPE_DOWNLOAD_COMPLETE = '1'
39EVENT_TYPE_INSTALL_COMPLETE = '2'
40EVENT_TYPE_UPDATE_COMPLETE = '3'
41EVENT_TYPE_DOWNLOAD_STARTED = '13'
42EVENT_TYPE_DOWNLOAD_FINISHED = '14'
43EVENT_TYPE_REBOOTED_AFTER_UPDATE = '54'
44
45# Update event results.
46EVENT_RESULT_ERROR = '0'
47EVENT_RESULT_SUCCESS = '1'
48EVENT_RESULT_SUCCESS_REBOOT = '2'
49EVENT_RESULT_UPDATE_DEFERRED = '9'
50
51# Omaha event types/results, from update_engine/omaha_request_action.h
52# These are stored in dict form in order to easily print out the keys.
53EVENT_TYPE_DICT = {
54        EVENT_TYPE_DOWNLOAD_COMPLETE: 'download_complete',
55        EVENT_TYPE_INSTALL_COMPLETE: 'install_complete',
56        EVENT_TYPE_UPDATE_COMPLETE: 'update_complete',
57        EVENT_TYPE_DOWNLOAD_STARTED: 'download_started',
58        EVENT_TYPE_DOWNLOAD_FINISHED: 'download_finished',
59        EVENT_TYPE_REBOOTED_AFTER_UPDATE: 'rebooted_after_update'
60}
61
62EVENT_RESULT_DICT = {
63        EVENT_RESULT_ERROR: 'error',
64        EVENT_RESULT_SUCCESS: 'success',
65        EVENT_RESULT_SUCCESS_REBOOT: 'success_reboot',
66        EVENT_RESULT_UPDATE_DEFERRED: 'update_deferred'
67}
68
69
70def snippet(text):
71    """Returns the text with start/end snip markers around it.
72
73    @param text: The snippet text.
74
75    @return The text with start/end snip markers around it.
76    """
77    snip = '---8<---' * 10
78    start = '-- START -'
79    end = '-- END -'
80    return ('%s%s\n%s\n%s%s' %
81            (start, snip[len(start):], text, end, snip[len(end):]))
82
83
84class ExpectedUpdateEvent(object):
85    """Defines an expected event in an update process."""
86
87    _ATTR_NAME_DICT_MAP = {
88            'event_type': EVENT_TYPE_DICT,
89            'event_result': EVENT_RESULT_DICT,
90    }
91
92    _VALID_TYPES = set(EVENT_TYPE_DICT.keys())
93    _VALID_RESULTS = set(EVENT_RESULT_DICT.keys())
94
95    def __init__(self, event_type=None, event_result=None, version=None,
96                 previous_version=None, on_error=None):
97        """Initializes an event expectation.
98
99        @param event_type: Expected event type.
100        @param event_result: Expected event result code.
101        @param version: Expected reported image version.
102        @param previous_version: Expected reported previous image version.
103        @param on_error: This is either an object to be returned when a received
104                         event mismatches the expectation, or a callable used
105                         for generating one. In the latter case, takes as
106                         input two attribute dictionaries (expected and actual)
107                         and an iterable of mismatched keys. If None, a generic
108                         message is returned.
109        """
110        if event_type and event_type not in self._VALID_TYPES:
111            raise ValueError('event_type %s is not valid.' % event_type)
112
113        if event_result and event_result not in self._VALID_RESULTS:
114            raise ValueError('event_result %s is not valid.' % event_result)
115
116        self._expected_attrs = {
117            'event_type': event_type,
118            'event_result': event_result,
119            'version': version,
120            'previous_version': previous_version,
121        }
122        self._on_error = on_error
123
124
125    @staticmethod
126    def _attr_val_str(attr_val, helper_dict, default=None):
127        """Returns an enriched attribute value string, or default."""
128        if not attr_val:
129            return default
130
131        s = str(attr_val)
132        if helper_dict:
133            s += ':%s' % helper_dict.get(attr_val, 'unknown')
134
135        return s
136
137
138    def _attr_name_and_values(self, attr_name, expected_attr_val,
139                              actual_attr_val=None):
140        """Returns an attribute name, expected and actual value strings.
141
142        This will return (name, expected, actual); the returned value for
143        actual will be None if its respective input is None/empty.
144
145        """
146        helper_dict = self._ATTR_NAME_DICT_MAP.get(attr_name)
147        expected_attr_val_str = self._attr_val_str(expected_attr_val,
148                                                   helper_dict,
149                                                   default='any')
150        actual_attr_val_str = self._attr_val_str(actual_attr_val, helper_dict)
151
152        return attr_name, expected_attr_val_str, actual_attr_val_str
153
154
155    def _attrs_to_str(self, attrs_dict):
156        return ' '.join(['%s=%s' %
157                         self._attr_name_and_values(attr_name, attr_val)[0:2]
158                         for attr_name, attr_val in attrs_dict.iteritems()])
159
160
161    def __str__(self):
162        return self._attrs_to_str(self._expected_attrs)
163
164
165    def verify(self, actual_event):
166        """Verify the attributes of an actual event.
167
168        @param actual_event: a dictionary containing event attributes
169
170        @return An error message, or None if all attributes as expected.
171
172        """
173        mismatched_attrs = [
174                attr_name for attr_name, expected_attr_val
175                in self._expected_attrs.iteritems()
176                if (expected_attr_val and
177                    not self._verify_attr(attr_name, expected_attr_val,
178                                          actual_event.get(attr_name)))]
179        if not mismatched_attrs:
180            return None
181        if callable(self._on_error):
182            return self._on_error(self._expected_attrs, actual_event,
183                                  mismatched_attrs)
184        if self._on_error is None:
185            return ('Received event (%s) does not match expectation (%s)' %
186                    (self._attrs_to_str(actual_event), self))
187        return self._on_error
188
189
190    def _verify_attr(self, attr_name, expected_attr_val, actual_attr_val):
191        """Verifies that an actual log event attributes matches expected on.
192
193        @param attr_name: name of the attribute to verify
194        @param expected_attr_val: expected attribute value
195        @param actual_attr_val: actual attribute value
196
197        @return True if actual value is present and matches, False otherwise.
198
199        """
200        # None values are assumed to be missing and non-matching.
201        if actual_attr_val is None:
202            logging.error('No value found for %s (expected %s)',
203                          *self._attr_name_and_values(attr_name,
204                                                      expected_attr_val)[0:2])
205            return False
206
207        # We allow expected version numbers (e.g. 2940.0.0) to be contained in
208        # actual values (2940.0.0-a1); this is necessary for the test to pass
209        # with developer / non-release images.
210        if (actual_attr_val == expected_attr_val or
211            ('version' in attr_name and expected_attr_val in actual_attr_val)):
212            return True
213
214        return False
215
216
217    def get_attrs(self):
218        """Returns a dictionary of expected attributes."""
219        return dict(self._expected_attrs)
220
221
222class ExpectedUpdateEventChain(object):
223    """Defines a chain of expected update events."""
224    def __init__(self):
225        self._expected_events_chain = []
226
227
228    def add_event(self, expected_events, timeout, on_timeout=None):
229        """Adds an expected event to the chain.
230
231        @param expected_events: The ExpectedEvent, or a list thereof, to wait
232                                for. If a list is passed, it will wait for *any*
233                                of the provided events, but only one of those.
234        @param timeout: A timeout (in seconds) to wait for the event.
235        @param on_timeout: An error string to use if the event times out. If
236                           None, a generic message is used.
237        """
238        if isinstance(expected_events, ExpectedUpdateEvent):
239            expected_events = [expected_events]
240        self._expected_events_chain.append(
241                (expected_events, timeout, on_timeout))
242
243
244    @staticmethod
245    def _format_event_with_timeout(expected_events, timeout):
246        """Returns a string representation of the event, with timeout."""
247        until = 'within %s seconds' % timeout if timeout else 'indefinitely'
248        return '%s, %s' % (' OR '.join(map(str, expected_events)), until)
249
250
251    def __str__(self):
252        return ('[%s]' %
253                ', '.join(
254                    [self._format_event_with_timeout(expected_events, timeout)
255                     for expected_events, timeout, _
256                     in self._expected_events_chain]))
257
258
259    def __repr__(self):
260        return str(self._expected_events_chain)
261
262
263    def verify(self, get_next_event):
264        """Verifies that an actual stream of events complies.
265
266        @param get_next_event: a function returning the next event
267
268        @raises ExpectedUpdateEventChainFailed if we failed to verify an event.
269
270        """
271        for expected_events, timeout, on_timeout in self._expected_events_chain:
272            logging.info('Expecting %s',
273                         self._format_event_with_timeout(expected_events,
274                                                         timeout))
275            err_msg = self._verify_event_with_timeout(
276                    expected_events, timeout, on_timeout, get_next_event)
277            if err_msg is not None:
278                logging.error('Failed expected event: %s', err_msg)
279                raise ExpectedUpdateEventChainFailed(err_msg)
280
281
282    @staticmethod
283    def _verify_event_with_timeout(expected_events, timeout, on_timeout,
284                                   get_next_event):
285        """Verify an expected event occurs within a given timeout.
286
287        @param expected_events: the list of possible events expected next
288        @param timeout: specified in seconds
289        @param on_timeout: A string to return if timeout occurs, or None.
290        @param get_next_event: function returning the next event in a stream
291
292        @return None if event complies, an error string otherwise.
293
294        """
295        base_timestamp = curr_timestamp = time.time()
296        expired_timestamp = base_timestamp + timeout
297        while curr_timestamp <= expired_timestamp:
298            new_event = get_next_event()
299            if new_event:
300                logging.info('Event received after %s seconds',
301                             round(curr_timestamp - base_timestamp, 1))
302                results = [event.verify(new_event) for event in expected_events]
303                return None if None in results else ' AND '.join(results)
304
305            # No new events, sleep for one second only (so we don't miss
306            # events at the end of the allotted timeout).
307            time.sleep(1)
308            curr_timestamp = time.time()
309
310        logging.error('Timeout expired')
311        if on_timeout is None:
312            return ('Waiting for event %s timed out after %d seconds' %
313                    (' OR '.join(map(str, expected_events)), timeout))
314        return on_timeout
315
316
317class UpdateEventLogVerifier(object):
318    """Verifies update event chains on a devserver update log."""
319    def __init__(self, event_log_url, url_request_timeout=None):
320        self._event_log_url = event_log_url
321        self._url_request_timeout = url_request_timeout
322        self._event_log = []
323        self._num_consumed_events = 0
324
325
326    def verify_expected_events_chain(self, expected_event_chain):
327        """Verify a given event chain.
328
329        @param expected_event_chain: instance of expected event chain.
330
331        @raises ExpectedUpdateEventChainFailed if we failed to verify the an
332                event.
333        """
334        expected_event_chain.verify(self._get_next_log_event)
335
336
337    def _get_next_log_event(self):
338        """Returns the next event in an event log.
339
340        Uses the URL handed to it during initialization to obtain the host log
341        from a devserver. If new events are encountered, the first of them is
342        consumed and returned.
343
344        @return The next new event in the host log, as reported by devserver;
345                None if no such event was found or an error occurred.
346
347        """
348        # (Re)read event log from devserver, if necessary.
349        if len(self._event_log) <= self._num_consumed_events:
350            try:
351                if self._url_request_timeout:
352                    conn = urllib2.urlopen(self._event_log_url,
353                                           timeout=self._url_request_timeout)
354                else:
355                    conn = urllib2.urlopen(self._event_log_url)
356            except urllib2.URLError, e:
357                logging.warning('Failed to read event log url: %s', e)
358                return None
359            except socket.timeout, e:
360                logging.warning('Timed out reading event log url: %s', e)
361                return None
362
363            event_log_resp = conn.read()
364            conn.close()
365            self._event_log = json.loads(event_log_resp)
366
367        # Return next new event, if one is found.
368        if len(self._event_log) > self._num_consumed_events:
369            new_event = {
370                    key: str(val) for key, val
371                    in self._event_log[self._num_consumed_events].iteritems()
372            }
373            self._num_consumed_events += 1
374            logging.info('Consumed new event: %s', new_event)
375            return new_event
376
377
378class OmahaDevserverFailedToStart(error.TestError):
379    """Raised when a omaha devserver fails to start."""
380
381
382class OmahaDevserver(object):
383    """Spawns a test-private devserver instance."""
384    # How long to wait for a devserver to start.
385    _WAIT_FOR_DEVSERVER_STARTED_SECONDS = 30
386
387    # How long to sleep (seconds) between checks to see if a devserver is up.
388    _WAIT_SLEEP_INTERVAL = 1
389
390    # Max devserver execution time (seconds); used with timeout(1) to ensure we
391    # don't have defunct instances hogging the system.
392    _DEVSERVER_TIMELIMIT_SECONDS = 12 * 60 * 60
393
394
395    def __init__(self, omaha_host, devserver_dir, update_payload_staged_url):
396        """Starts a private devserver instance, operating at Omaha capacity.
397
398        @param omaha_host: host address where the devserver is spawned.
399        @param devserver_dir: path to the devserver source directory
400        @param update_payload_staged_url: URL to provision for update requests.
401
402        """
403        if not update_payload_staged_url:
404            raise error.TestError('Missing update payload url')
405
406        self._omaha_host = omaha_host
407        self._devserver_pid = 0
408        self._devserver_port = 0  # Determined later from devserver portfile.
409        self._devserver_dir = devserver_dir
410        self._update_payload_staged_url = update_payload_staged_url
411
412        self._devserver_ssh = hosts.SSHHost(self._omaha_host,
413                                            user=os.environ['USER'])
414
415        # Temporary files for various devserver outputs.
416        self._devserver_logfile = None
417        self._devserver_stdoutfile = None
418        self._devserver_portfile = None
419        self._devserver_pidfile = None
420        self._devserver_static_dir = None
421
422
423    def _cleanup_devserver_files(self):
424        """Cleans up the temporary devserver files."""
425        for filename in (self._devserver_logfile, self._devserver_stdoutfile,
426                         self._devserver_portfile, self._devserver_pidfile):
427            if filename:
428                self._devserver_ssh.run('rm -f %s' % filename,
429                                        ignore_status=True)
430
431        if self._devserver_static_dir:
432            self._devserver_ssh.run('rm -rf %s' % self._devserver_static_dir,
433                                    ignore_status=True)
434
435
436    def _create_tempfile_on_devserver(self, label, dir=False):
437        """Creates a temporary file/dir on the devserver and returns its path.
438
439        @param label: Identifier for the file context (string, no whitespaces).
440        @param dir: If True, create a directory instead of a file.
441
442        @raises test.TestError: If we failed to invoke mktemp on the server.
443        @raises OmahaDevserverFailedToStart: If tempfile creation failed.
444        """
445        remote_cmd = 'mktemp --tmpdir devserver-%s.XXXXXX' % label
446        if dir:
447            remote_cmd += ' --directory'
448
449        try:
450            result = self._devserver_ssh.run(remote_cmd, ignore_status=True)
451        except error.AutoservRunError as e:
452            self._log_and_raise_remote_ssh_error(e)
453        if result.exit_status != 0:
454            raise OmahaDevserverFailedToStart(
455                    'Could not create a temporary %s file on the devserver, '
456                    'error output: "%s"' % (label, result.stderr))
457        return result.stdout.strip()
458
459    @staticmethod
460    def _log_and_raise_remote_ssh_error(e):
461        """Logs failure to ssh remote, then raises a TestError."""
462        logging.debug('Failed to ssh into the devserver: %s', e)
463        logging.error('If you are running this locally it means you did not '
464                      'configure ssh correctly.')
465        raise error.TestError('Failed to ssh into the devserver: %s' % e)
466
467
468    def _read_int_from_devserver_file(self, filename):
469        """Reads and returns an integer value from a file on the devserver."""
470        return int(self._get_devserver_file_content(filename).strip())
471
472
473    def _wait_for_devserver_to_start(self):
474        """Waits until the devserver starts within the time limit.
475
476        Infers and sets the devserver PID and serving port.
477
478        Raises:
479            OmahaDevserverFailedToStart: If the time limit is reached and we
480                                         cannot connect to the devserver.
481        """
482        # Compute the overall timeout.
483        deadline = time.time() + self._WAIT_FOR_DEVSERVER_STARTED_SECONDS
484
485        # First, wait for port file to be filled and determine the server port.
486        logging.warning('Waiting for devserver to start up.')
487        while time.time() < deadline:
488            try:
489                self._devserver_pid = self._read_int_from_devserver_file(
490                        self._devserver_pidfile)
491                self._devserver_port = self._read_int_from_devserver_file(
492                        self._devserver_portfile)
493                logging.info('Devserver pid is %d, serving on port %d',
494                             self._devserver_pid, self._devserver_port)
495                break
496            except Exception:  # Couldn't read file or corrupt content.
497                time.sleep(self._WAIT_SLEEP_INTERVAL)
498        else:
499            try:
500                self._devserver_ssh.run_output('uptime')
501            except error.AutoservRunError as e:
502                logging.debug('Failed to run uptime on the devserver: %s', e)
503            raise OmahaDevserverFailedToStart(
504                    'The test failed to find the pid/port of the omaha '
505                    'devserver after %d seconds. Check the dumped devserver '
506                    'logs and devserver load for more information.' %
507                    self._WAIT_FOR_DEVSERVER_STARTED_SECONDS)
508
509        # Check that the server is reponsding to network requests.
510        logging.warning('Waiting for devserver to accept network requests.')
511        url = 'http://%s' % self.get_netloc()
512        while time.time() < deadline:
513            if dev_server.ImageServer.devserver_healthy(url, timeout_min=0.1):
514                break
515
516            # TODO(milleral): Refactor once crbug.com/221626 is resolved.
517            time.sleep(self._WAIT_SLEEP_INTERVAL)
518        else:
519            raise OmahaDevserverFailedToStart(
520                    'The test failed to establish a connection to the omaha '
521                    'devserver it set up on port %d. Check the dumped '
522                    'devserver logs for more information.' %
523                    self._devserver_port)
524
525
526    def start_devserver(self):
527        """Starts the devserver and confirms it is up.
528
529        Raises:
530            test.TestError: If we failed to spawn the remote devserver.
531            OmahaDevserverFailedToStart: If the time limit is reached and we
532                                         cannot connect to the devserver.
533        """
534        update_payload_url_base, update_payload_path = self._split_url(
535                self._update_payload_staged_url)
536
537        # Allocate temporary files for various server outputs.
538        self._devserver_logfile = self._create_tempfile_on_devserver('log')
539        self._devserver_stdoutfile = self._create_tempfile_on_devserver(
540                'stdout')
541        self._devserver_portfile = self._create_tempfile_on_devserver('port')
542        self._devserver_pidfile = self._create_tempfile_on_devserver('pid')
543        self._devserver_static_dir = self._create_tempfile_on_devserver(
544                'static', dir=True)
545
546        # Invoke the Omaha/devserver on the remote server. Will attempt to kill
547        # it with a SIGTERM after a predetermined timeout has elapsed, followed
548        # by SIGKILL if not dead within 30 seconds from the former signal.
549        cmdlist = [
550                'timeout', '-s', 'TERM', '-k', '30',
551                str(self._DEVSERVER_TIMELIMIT_SECONDS),
552                '%s/devserver.py' % self._devserver_dir,
553                '--payload=%s' % update_payload_path,
554                '--port=0',
555                '--pidfile=%s' % self._devserver_pidfile,
556                '--portfile=%s' % self._devserver_portfile,
557                '--logfile=%s' % self._devserver_logfile,
558                '--remote_payload',
559                '--urlbase=%s' % update_payload_url_base,
560                '--max_updates=1',
561                '--host_log',
562                '--static_dir=%s' % self._devserver_static_dir,
563                '--critical_update',
564        ]
565        remote_cmd = '( %s ) </dev/null >%s 2>&1 &' % (
566                ' '.join(cmdlist), self._devserver_stdoutfile)
567
568        logging.info('Starting devserver with %r', remote_cmd)
569        try:
570            self._devserver_ssh.run_output(remote_cmd)
571        except error.AutoservRunError as e:
572            self._log_and_raise_remote_ssh_error(e)
573
574        try:
575            self._wait_for_devserver_to_start()
576        except OmahaDevserverFailedToStart:
577            self._kill_remote_process()
578            self._dump_devserver_log()
579            self._cleanup_devserver_files()
580            raise
581
582
583    def _kill_remote_process(self):
584        """Kills the devserver and verifies it's down; clears the remote pid."""
585        def devserver_down():
586            """Ensure that the devserver process is down."""
587            return not self._remote_process_alive()
588
589        if devserver_down():
590            return
591
592        for signal in 'SIGTERM', 'SIGKILL':
593            remote_cmd = 'kill -s %s %s' % (signal, self._devserver_pid)
594            self._devserver_ssh.run(remote_cmd)
595            try:
596                client_utils.poll_for_condition(
597                        devserver_down, sleep_interval=1, desc='devserver down')
598                break
599            except client_utils.TimeoutError:
600                logging.warning('Could not kill devserver with %s.', signal)
601        else:
602            logging.warning('Failed to kill devserver, giving up.')
603
604        self._devserver_pid = None
605
606
607    def _remote_process_alive(self):
608        """Tests whether the remote devserver process is running."""
609        if not self._devserver_pid:
610            return False
611        remote_cmd = 'test -e /proc/%s' % self._devserver_pid
612        result = self._devserver_ssh.run(remote_cmd, ignore_status=True)
613        return result.exit_status == 0
614
615
616    def get_netloc(self):
617        """Returns the netloc (host:port) of the devserver."""
618        if not (self._devserver_pid and self._devserver_port):
619            raise error.TestError('No running omaha/devserver')
620
621        return '%s:%s' % (self._omaha_host, self._devserver_port)
622
623
624    def get_update_url(self):
625        """Returns the update_url you can use to update via this server."""
626        return urlparse.urlunsplit(('http', self.get_netloc(), '/update',
627                                    '', ''))
628
629
630    def _get_devserver_file_content(self, filename):
631        """Returns the content of a file on the devserver."""
632        return self._devserver_ssh.run_output('cat %s' % filename,
633                                              stdout_tee=None)
634
635
636    def _get_devserver_log(self):
637        """Obtain the devserver output."""
638        return self._get_devserver_file_content(self._devserver_logfile)
639
640
641    def _get_devserver_stdout(self):
642        """Obtain the devserver output in stdout and stderr."""
643        return self._get_devserver_file_content(self._devserver_stdoutfile)
644
645
646    def _dump_devserver_log(self, logging_level=logging.ERROR):
647        """Dump the devserver log to the autotest log.
648
649        @param logging_level: logging level (from logging) to log the output.
650        """
651        logging.log(logging_level, "Devserver stdout and stderr:\n" +
652                    snippet(self._get_devserver_stdout()))
653        logging.log(logging_level, "Devserver log file:\n" +
654                    snippet(self._get_devserver_log()))
655
656
657    @staticmethod
658    def _split_url(url):
659        """Splits a URL into the URL base and path."""
660        split_url = urlparse.urlsplit(url)
661        url_base = urlparse.urlunsplit(
662                (split_url.scheme, split_url.netloc, '', '', ''))
663        url_path = split_url.path
664        return url_base, url_path.lstrip('/')
665
666
667    def stop_devserver(self):
668        """Kill remote process and wait for it to die, dump its output."""
669        if not self._devserver_pid:
670            logging.error('No running omaha/devserver.')
671            return
672
673        logging.info('Killing omaha/devserver')
674        self._kill_remote_process()
675        logging.debug('Final devserver log before killing')
676        self._dump_devserver_log(logging.DEBUG)
677        self._cleanup_devserver_files()
678
679
680class TestPlatform(object):
681    """An interface and factory for platform-dependent functionality."""
682
683    # Named tuple containing urls for staged urls needed for test.
684    # source_url: url to find the update payload for the source image.
685    # source_stateful_url: url to find the stateful payload for the source
686    #                      image.
687    # target_url: url to find the update payload for the target image.
688    # target_stateful_url: url to find the stateful payload for the target
689    #                      image.
690    StagedURLs = collections.namedtuple(
691            'StagedURLs',
692            ['source_url', 'source_stateful_url', 'target_url',
693             'target_stateful_url'])
694
695
696    def __init__(self):
697        assert False, 'Cannot instantiate this interface'
698
699
700    @staticmethod
701    def create(host):
702        """Returns a TestPlatform implementation based on the host type.
703
704        *DO NOT* override this method.
705
706        @param host: a host object representing the DUT
707
708        @return A TestPlatform implementation.
709        """
710        os_type = host.get_os_type()
711        if os_type == 'cros':
712            return ChromiumOSTestPlatform(host)
713        if os_type == 'brillo':
714            return BrilloTestPlatform(host)
715
716        raise error.TestError('Unknown OS type reported by host: %s' % os_type)
717
718
719    def initialize(self, autotest_devserver, devserver_dir):
720        """Initialize the object.
721
722        @param autotest_devserver: Instance of client.common_lib.dev_server to
723                                   use to reach the devserver instance for this
724                                   build.
725        @param devserver_dir: Path to devserver source tree.
726        """
727        raise NotImplementedError
728
729
730    def prep_artifacts(self, test_conf):
731        """Prepares update artifacts for the test.
732
733        The test config must include 'source_payload_uri' and
734        'target_payload_uri'. In addition, it may include platform-specific
735        values as determined by the test control file.
736
737        @param test_conf: Dictionary containing the test configuration.
738
739        @return A tuple of staged URLs.
740
741        @raise error.TestError on failure.
742        """
743        raise NotImplementedError
744
745
746    def reboot_device(self):
747        """Reboots the device."""
748        raise NotImplementedError
749
750
751    def prep_device_for_update(self, source_release):
752        """Prepares the device for update.
753
754        @param source_release: Source release version (string), or None.
755
756        @raise error.TestError on failure.
757        """
758        raise NotImplementedError
759
760
761    def get_active_slot(self):
762        """Returns the active boot slot of the device."""
763        raise NotImplementedError
764
765
766    def start_update_perf(self, bindir):
767        """Starts performance monitoring (if available).
768
769        @param bindir: Directory containing test binary files.
770        """
771        raise NotImplementedError
772
773
774    def stop_update_perf(self):
775        """Stops performance monitoring and returns data (if available).
776
777        @return Dictionary containing performance attributes.
778        """
779        raise NotImplementedError
780
781
782    def trigger_update(self, omaha_devserver):
783        """Kicks off an update.
784
785        @param omaha_devserver: OmahaDevserver instance.
786        """
787        raise NotImplementedError
788
789
790    def finalize_update(self):
791        """Performs post-update procedures."""
792        raise NotImplementedError
793
794
795    def get_update_log(self, num_lines):
796        """Returns the update log.
797
798        @param num_lines: Number of log lines to return (tail), zero for all.
799
800        @return String containing the last |num_lines| from the update log.
801        """
802        raise NotImplementedError
803
804
805    def check_device_after_update(self, target_release):
806        """Runs final sanity checks.
807
808        @param target_release: Target release version (string), or None.
809
810        @raise error.TestError on failure.
811        """
812        raise NotImplementedError
813
814
815class ChromiumOSTestPlatform(TestPlatform):
816    """A TestPlatform implementation for Chromium OS."""
817
818    _STATEFUL_UPDATE_FILENAME = 'stateful.tgz'
819    _LOGINABLE_MINIMUM_RELEASE = 5110
820
821    def __init__(self, host):
822        self._host = host
823        self._autotest_devserver = None
824        self._devserver_dir = None
825        self._staged_urls = None
826        self._perf_mon_pid = None
827
828
829    def _stage_payload(self, devserver_label, filename, archive_url=None):
830        """Stage the given payload onto the devserver.
831
832        Works for either a stateful or full/delta test payload. Expects the
833        gs_path or a combo of devserver_label + filename.
834
835        @param devserver_label: The build name e.g. x86-mario-release/<version>.
836                                If set, assumes default gs archive bucket and
837                                requires filename to be specified.
838        @param filename: In conjunction with devserver_label, if just specifying
839                         the devserver label name, this is which file are you
840                         downloading.
841        @param archive_url: An optional GS archive location, if not using the
842                            devserver's default.
843
844        @return URL of the staged payload on the server.
845
846        @raise error.TestError if there's a problem with staging.
847
848        """
849        try:
850            self._autotest_devserver.stage_artifacts(
851                    image=devserver_label, files=[filename],
852                    archive_url=archive_url)
853            return self._autotest_devserver.get_staged_file_url(filename,
854                                                                devserver_label)
855        except dev_server.DevServerException, e:
856            raise error.TestError('Failed to stage payload: %s' % e)
857
858
859    def _stage_payload_by_uri(self, payload_uri):
860        """Stage a payload based on its GS URI.
861
862        This infers the build's label, filename and GS archive from the
863        provided GS URI.
864
865        @param payload_uri: The full GS URI of the payload.
866
867        @return URL of the staged payload on the server.
868
869        @raise error.TestError if there's a problem with staging.
870
871        """
872        archive_url, _, filename = payload_uri.rpartition('/')
873        devserver_label = urlparse.urlsplit(archive_url).path.strip('/')
874        return self._stage_payload(devserver_label, filename,
875                                   archive_url=archive_url)
876
877
878    @staticmethod
879    def _payload_to_update_url(payload_url):
880        """Given a update or stateful payload url, returns the update url."""
881        # We want to transform it to the correct omaha url which is
882        # <hostname>/update/...LABEL.
883        base_url = payload_url.rpartition('/')[0]
884        return base_url.replace('/static/', '/update/')
885
886
887    def _get_stateful_uri(self, build_uri):
888        """Returns a complete GS URI of a stateful update given a build path."""
889        return '/'.join([build_uri.rstrip('/'), self._STATEFUL_UPDATE_FILENAME])
890
891
892    def _payload_to_stateful_uri(self, payload_uri):
893        """Given a payload GS URI, returns the corresponding stateful URI."""
894        build_uri = payload_uri.rpartition('/')[0]
895        if build_uri.endswith('payloads'):
896            build_uri = build_uri.rpartition('/')[0]
897        return self._get_stateful_uri(build_uri)
898
899
900    def _update_via_test_payloads(self, omaha_host, payload_url, stateful_url,
901                                  clobber):
902        """Given the following update and stateful urls, update the DUT.
903
904        Only updates the rootfs/stateful if the respective url is provided.
905
906        @param omaha_host: If updating rootfs, redirect updates through this
907            host. Should be None iff payload_url is None.
908        @param payload_url: If set, the specified url to find the update
909            payload.
910        @param stateful_url: If set, the specified url to find the stateful
911            payload.
912        @param clobber: If True, do a clean install of stateful.
913        """
914        def perform_update(url, is_stateful):
915            """Perform a rootfs/stateful update using given URL.
916
917            @param url: URL to update from.
918            @param is_stateful: Whether this is a stateful or rootfs update.
919            """
920            if url:
921                updater = autoupdater.ChromiumOSUpdater(url, host=self._host)
922                if is_stateful:
923                    updater.update_stateful(clobber=clobber)
924                else:
925                    updater.update_image()
926
927        # We create a OmahaDevserver to redirect blah.bin to update/. This
928        # allows us to use any payload filename to serve an update.
929        temp_devserver = None
930        try:
931            if payload_url:
932                temp_devserver = OmahaDevserver(
933                        omaha_host, self._devserver_dir, payload_url)
934                temp_devserver.start_devserver()
935                payload_url = temp_devserver.get_update_url()
936
937            stateful_url = self._payload_to_update_url(stateful_url)
938
939            perform_update(payload_url, False)
940            perform_update(stateful_url, True)
941        finally:
942            if temp_devserver:
943                temp_devserver.stop_devserver()
944
945
946    def _install_source_version(self, devserver_hostname, image_url,
947                                stateful_url):
948        """Prepare the specified host with the image given by the urls.
949
950        @param devserver_hostname: If updating rootfs, redirect updates
951                                   through this host. Should be None iff
952                                   image_url is None.
953        @param image_url: If set, the specified url to find the source image
954                          or full payload for the source image.
955        @param stateful_url: If set, the specified url to find the stateful
956                             payload.
957        """
958        try:
959            # Reboot to get us into a clean state.
960            self._host.reboot()
961            # Since we are installing the source image of the test, clobber
962            # stateful.
963            self._update_via_test_payloads(devserver_hostname, image_url,
964                                           stateful_url, True)
965            self._host.reboot()
966        except OmahaDevserverFailedToStart as e:
967            logging.fatal('Failed to start private devserver for installing '
968                          'the source image (%s) on the DUT', image_url)
969            raise error.TestError(
970                    'Failed to start private devserver for installing the '
971                    'source image on the DUT: %s' % e)
972        except error.AutoservRunError as e:
973            logging.fatal('Error re-imaging or rebooting the DUT with the '
974                          'source image from %s', image_url)
975            raise error.TestError('Failed to install the source image or '
976                                  'reboot the DUT: %s' % e)
977
978
979    def _stage_artifacts_onto_devserver(self, test_conf):
980        """Stages artifacts that will be used by the test onto the devserver.
981
982        @param test_conf: a dictionary containing test configuration values
983
984        @return a StagedURLs tuple containing the staged urls.
985        """
986        logging.info('Staging images onto autotest devserver (%s)',
987                     self._autotest_devserver.url())
988
989        staged_source_url = None
990        staged_source_stateful_url = None
991        try:
992            source_payload_uri = test_conf['source_payload_uri']
993        except KeyError:
994            # TODO(garnold) Remove legacy key support once control files on all
995            # release branches have caught up.
996            source_payload_uri = test_conf['source_image_uri']
997        if source_payload_uri:
998            staged_source_url = self._stage_payload_by_uri(source_payload_uri)
999
1000            # In order to properly install the source image using a full
1001            # payload we'll also need the stateful update that comes with it.
1002            # In general, tests may have their source artifacts in a different
1003            # location than their payloads. This is determined by whether or
1004            # not the source_archive_uri attribute is set; if it isn't set,
1005            # then we derive it from the dirname of the source payload.
1006            source_archive_uri = test_conf.get('source_archive_uri')
1007            if source_archive_uri:
1008                source_stateful_uri = self._get_stateful_uri(source_archive_uri)
1009            else:
1010                source_stateful_uri = self._payload_to_stateful_uri(
1011                        source_payload_uri)
1012
1013            staged_source_stateful_url = self._stage_payload_by_uri(
1014                    source_stateful_uri)
1015
1016            # Log source image URLs.
1017            logging.info('Source full payload from %s staged at %s',
1018                         source_payload_uri, staged_source_url)
1019            if staged_source_stateful_url:
1020                logging.info('Source stateful update from %s staged at %s',
1021                             source_stateful_uri, staged_source_stateful_url)
1022
1023        target_payload_uri = test_conf['target_payload_uri']
1024        staged_target_url = self._stage_payload_by_uri(target_payload_uri)
1025        target_stateful_uri = None
1026        staged_target_stateful_url = None
1027        target_archive_uri = test_conf.get('target_archive_uri')
1028        if target_archive_uri:
1029            target_stateful_uri = self._get_stateful_uri(target_archive_uri)
1030        else:
1031            # Attempt to get the job_repo_url to find the stateful payload for
1032            # the target image.
1033            job_repo_url = afe_utils.get_host_attribute(
1034                    self._host, self._host.job_repo_url_attribute)
1035            if not job_repo_url:
1036                target_stateful_uri = self._payload_to_stateful_uri(
1037                    target_payload_uri)
1038            else:
1039                _, devserver_label = tools.get_devserver_build_from_package_url(
1040                        job_repo_url)
1041                staged_target_stateful_url = self._stage_payload(
1042                        devserver_label, self._STATEFUL_UPDATE_FILENAME)
1043
1044        if not staged_target_stateful_url and target_stateful_uri:
1045            staged_target_stateful_url = self._stage_payload_by_uri(
1046                    target_stateful_uri)
1047
1048        # Log target payload URLs.
1049        logging.info('%s test payload from %s staged at %s',
1050                     test_conf['update_type'], target_payload_uri,
1051                     staged_target_url)
1052        logging.info('Target stateful update from %s staged at %s',
1053                     target_stateful_uri or 'standard location',
1054                     staged_target_stateful_url)
1055
1056        return self.StagedURLs(staged_source_url, staged_source_stateful_url,
1057                               staged_target_url, staged_target_stateful_url)
1058
1059
1060    def _run_login_test(self, release_string):
1061        """Runs login_LoginSuccess test if it is supported by the release."""
1062        # Only do login tests with recent builds, since they depend on
1063        # some binary compatibility with the build itself.
1064        # '5116.0.0' -> ('5116', '0', '0') -> 5116
1065        if not release_string:
1066            logging.info('No release provided, skipping login test.')
1067        elif int(release_string.split('.')[0]) > self._LOGINABLE_MINIMUM_RELEASE:
1068            # Login, to prove we can before/after the update.
1069            logging.info('Attempting to login (release %s).', release_string)
1070
1071            client_at = autotest.Autotest(self._host)
1072            client_at.run_test('login_LoginSuccess', arc_mode='enabled')
1073        else:
1074            logging.info('Not attempting login test because %s is older than '
1075                         '%d.', release_string, self._LOGINABLE_MINIMUM_RELEASE)
1076
1077
1078    def _start_perf_mon(self, bindir):
1079        """Starts monitoring performance and resource usage on a DUT.
1080
1081        Call _stop_perf_mon() with the returned PID to stop monitoring
1082        and collect the results.
1083
1084        @param bindir: Directoy containing monitoring script.
1085
1086        @return The PID of the newly created DUT monitoring process.
1087        """
1088        # We can't assume much about the source image so we copy the
1089        # performance monitoring script to the DUT directly.
1090        path = os.path.join(bindir, 'update_engine_performance_monitor.py')
1091        self._host.send_file(path, '/tmp')
1092        cmd = 'python /tmp/update_engine_performance_monitor.py --start-bg'
1093        return int(self._host.run(cmd).stdout)
1094
1095
1096    def _stop_perf_mon(self, perf_mon_pid):
1097        """Stops monitoring performance and resource usage on a DUT.
1098
1099        @param perf_mon_pid: the PID returned from _start_perf_mon().
1100
1101        @return Dictionary containing performance attributes, or None if
1102                unavailable.
1103        """
1104        # Gracefully handle problems with performance monitoring by
1105        # just returning None.
1106        try:
1107            cmd = ('python /tmp/update_engine_performance_monitor.py '
1108                   '--stop-bg=%d') % perf_mon_pid
1109            perf_json_txt = self._host.run(cmd).stdout
1110            return json.loads(perf_json_txt)
1111        except Exception as e:
1112            logging.warning('Failed to parse output from '
1113                            'update_engine_performance_monitor.py: %s', e)
1114        return None
1115
1116
1117    # Interface overrides.
1118    #
1119    def initialize(self, autotest_devserver, devserver_dir):
1120        self._autotest_devserver = autotest_devserver
1121        self._devserver_dir = devserver_dir
1122
1123
1124    def reboot_device(self):
1125        self._host.reboot()
1126
1127
1128    def prep_artifacts(self, test_conf):
1129        self._staged_urls = self._stage_artifacts_onto_devserver(test_conf)
1130        return self._staged_urls
1131
1132
1133    def prep_device_for_update(self, source_release):
1134        # Install the source version onto the DUT.
1135        if self._staged_urls.source_url:
1136            logging.info('Installing a source image on the DUT')
1137            devserver_hostname = urlparse.urlparse(
1138                    self._autotest_devserver.url()).hostname
1139            self._install_source_version(devserver_hostname,
1140                                         self._staged_urls.source_url,
1141                                         self._staged_urls.source_stateful_url)
1142
1143        # Make sure we can login before the update.
1144        self._run_login_test(source_release)
1145
1146
1147    def get_active_slot(self):
1148        return self._host.run('rootdev -s').stdout.strip()
1149
1150
1151    def start_update_perf(self, bindir):
1152        if self._perf_mon_pid is None:
1153            self._perf_mon_pid = self._start_perf_mon(bindir)
1154
1155
1156    def stop_update_perf(self):
1157        perf_data = None
1158        if self._perf_mon_pid is not None:
1159            perf_data = self._stop_perf_mon(self._perf_mon_pid)
1160            self._perf_mon_pid = None
1161
1162        return perf_data
1163
1164
1165    def trigger_update(self, omaha_devserver):
1166        updater = autoupdater.ChromiumOSUpdater(
1167                omaha_devserver.get_update_url(), host=self._host)
1168        updater.trigger_update()
1169
1170
1171    def finalize_update(self):
1172        self._update_via_test_payloads(
1173                None, None, self._staged_urls.target_stateful_url, False)
1174
1175
1176    def get_update_log(self, num_lines):
1177        return self._host.run_output(
1178                'tail -n %d /var/log/update_engine.log' % num_lines,
1179                stdout_tee=None)
1180
1181
1182    def check_device_after_update(self, target_release):
1183        # Make sure we can login after update.
1184        self._run_login_test(target_release)
1185
1186
1187class BrilloTestPlatform(TestPlatform):
1188    """A TestPlatform implementation for Brillo."""
1189
1190    _URL_DEFAULT_PORT = 80
1191    _DUT_LOCALHOST = '127.0.0.1'
1192
1193    def __init__(self, host):
1194        self._host = host
1195        self._autotest_devserver = None
1196        self._devserver_dir = None
1197        self._staged_urls = None
1198        self._forwarding_ports = set()
1199
1200
1201    @classmethod
1202    def _get_host_port(cls, url):
1203        """Returns the host and port values from a given URL.
1204
1205        @param url: The URL from which the values are extracted.
1206
1207        @return A pair consisting of the host and port strings.
1208        """
1209        host, _, port = urlparse.urlsplit(url).netloc.partition(':')
1210        return host, port or str(cls._URL_DEFAULT_PORT)
1211
1212
1213    def _install_rev_forwarding(self, port=None):
1214        """Installs reverse forwarding rules via ADB.
1215
1216        @param port: The TCP port we want forwarded; if None, installs all
1217                     previously configured ports.
1218        """
1219        ports = self._forwarding_ports if port is None else [port]
1220        for port in ports:
1221            port_spec = 'tcp:%s' % port
1222            self._host.add_forwarding(port_spec, port_spec, reverse=True)
1223
1224
1225    def _add_rev_forwarding(self, url):
1226        """Configures reverse port forwarding and adjusts the given URL.
1227
1228        This extracts the port from the URL, adds it to the set of configured
1229        forwarding ports, installs it to the DUT, then returns the adjusted URL
1230        for use by the DUT.
1231
1232        @param url: The URL for which we need to establish forwarding.
1233
1234        @return: The adjusted URL for use on the DUT.
1235        """
1236        if url:
1237            host, port = self._get_host_port(url)
1238            if port not in self._forwarding_ports:
1239                self._forwarding_ports.add(port)
1240                self._install_rev_forwarding(port=port)
1241            url = url.replace(host, self._DUT_LOCALHOST, 1)
1242        return url
1243
1244
1245    def _remove_rev_forwarding(self, url=None):
1246        """Removes a reverse port forwarding.
1247
1248        @param url: The URL for which forwarding was established; if None,
1249                    removes all previously configured ports.
1250        """
1251        ports = set()
1252        if url is None:
1253            ports.update(self._forwarding_ports)
1254        else:
1255            _, port = self._get_host_port(url)
1256            if port in self._forwarding_ports:
1257                ports.add(port)
1258
1259        # TODO(garnold) Enable once ADB port removal is fixed (b/24771474):
1260        # for port in ports:
1261        #     self._host.remove_forwarding(src='tcp:%s' % port, reverse=True)
1262
1263        self._forwarding_ports.difference_update(ports)
1264
1265
1266    def _install_source_version(self, devserver_hostname, payload_url):
1267        """Installs a source version onto the test device.
1268
1269        @param devserver_hostname: Redirect updates through this host.
1270        @param payload_url: URL of staged payload for installing a source image.
1271        """
1272        try:
1273            # Start a private Omaha server and update the DUT.
1274            temp_devserver = None
1275            url = None
1276            try:
1277                temp_devserver = OmahaDevserver(
1278                        devserver_hostname, self._devserver_dir, payload_url)
1279                temp_devserver.start_devserver()
1280                url = self._add_rev_forwarding(temp_devserver.get_update_url())
1281                updater = autoupdater.BrilloUpdater(url, host=self._host)
1282                updater.update_image()
1283            finally:
1284                if url:
1285                    self._remove_rev_forwarding(url)
1286                if temp_devserver:
1287                    temp_devserver.stop_devserver()
1288
1289            # Reboot the DUT.
1290            self.reboot_device()
1291        except OmahaDevserverFailedToStart as e:
1292            logging.fatal('Failed to start private devserver for installing '
1293                          'the source payload (%s) on the DUT', payload_url)
1294            raise error.TestError(
1295                    'Failed to start private devserver for installing the '
1296                    'source image on the DUT: %s' % e)
1297        except error.AutoservRunError as e:
1298            logging.fatal('Error re-imaging or rebooting the DUT with the '
1299                          'source image from %s', payload_url)
1300            raise error.TestError('Failed to install the source image or '
1301                                  'reboot the DUT: %s' % e)
1302
1303
1304    # Interface overrides.
1305    #
1306    def initialize(self, autotest_devserver, devserver_dir):
1307        self._autotest_devserver = autotest_devserver
1308        self._devserver_dir = devserver_dir
1309
1310
1311    def reboot_device(self):
1312        self._host.reboot()
1313        self._install_rev_forwarding()
1314
1315
1316    def prep_artifacts(self, test_conf):
1317        # TODO(garnold) Currently we don't stage anything and assume that the
1318        # provided URLs are of pre-staged payloads. We should implement staging
1319        # support once a release scheme for Brillo is finalized.
1320        self._staged_urls = self.StagedURLs(
1321                self._add_rev_forwarding(test_conf['source_payload_uri']), None,
1322                self._add_rev_forwarding(test_conf['target_payload_uri']), None)
1323        return self._staged_urls
1324
1325
1326    def prep_device_for_update(self, source_release):
1327        # Install the source version onto the DUT.
1328        if self._staged_urls.source_url:
1329            logging.info('Installing a source image on the DUT')
1330            devserver_hostname = urlparse.urlparse(
1331                    self._autotest_devserver.url()).hostname
1332            self._install_source_version(devserver_hostname,
1333                                         self._staged_urls.source_url)
1334
1335
1336    def get_active_slot(self):
1337        return self._host.run('rootdev -s /system').stdout.strip()
1338
1339
1340    def start_update_perf(self, bindir):
1341        pass
1342
1343
1344    def stop_update_perf(self):
1345        pass
1346
1347
1348    def trigger_update(self, omaha_devserver):
1349        url = self._add_rev_forwarding(omaha_devserver.get_update_url())
1350        updater = autoupdater.BrilloUpdater(url, host=self._host)
1351        updater.trigger_update()
1352
1353
1354    def finalize_update(self):
1355        pass
1356
1357
1358    def get_update_log(self, num_lines):
1359        return self._host.run_output(
1360                'logcat -d -s update_engine | tail -n %d' % num_lines,
1361                stdout_tee=None)
1362
1363
1364    def check_device_after_update(self, target_release):
1365        self._remove_rev_forwarding()
1366        # TODO(garnold) Port forwarding removal is broken in ADB (b/24771474).
1367        # Instead we reboot the device, which has the side-effect of flushing
1368        # all forwarding rules. Once fixed, the following should be removed.
1369        self.reboot_device()
1370
1371
1372class autoupdate_EndToEndTest(test.test):
1373    """Complete update test between two Chrome OS releases.
1374
1375    Performs an end-to-end test of updating a ChromeOS device from one version
1376    to another. The test performs the following steps:
1377
1378      1. Stages the source (full) and target update payload on the central
1379         devserver.
1380      2. Spawns a private Omaha-like devserver instance, configured to return
1381         the target (update) payload URL in response for an update check.
1382      3. Reboots the DUT.
1383      4. Installs a source image on the DUT (if provided) and reboots to it.
1384      5. Triggers an update check at the DUT.
1385      6. Watches as the DUT obtains an update and applies it.
1386      7. Reboots and repeats steps 5-6, ensuring that the next update check
1387         shows the new image version.
1388
1389    Some notes on naming:
1390      devserver: Refers to a machine running the Chrome OS Update Devserver.
1391      autotest_devserver: An autotest wrapper to interact with a devserver.
1392                          Can be used to stage artifacts to a devserver. While
1393                          this can also be used to update a machine, we do not
1394                          use it for that purpose in this test as we manage
1395                          updates with out own devserver instances (see below).
1396      omaha_devserver: This test's wrapper of a devserver running for the
1397                       purposes of emulating omaha. This test controls the
1398                       lifetime of this devserver instance and is separate
1399                       from the autotest lab's devserver's instances which are
1400                       only used for staging and hosting artifacts (because they
1401                       scale). These are run on the same machines as the actual
1402                       autotest devservers which are used for staging but on
1403                       different ports.
1404      *staged_url's: In this case staged refers to the fact that these items
1405                     are available to be downloaded statically from these urls
1406                     e.g. 'localhost:8080/static/my_file.gz'. These are usually
1407                     given after staging an artifact using a autotest_devserver
1408                     though they can be re-created given enough assumptions.
1409      *update_url's: Urls refering to the update RPC on a given omaha devserver.
1410                     Since we always use an instantiated omaha devserver to run
1411                     updates, these will always reference an existing instance
1412                     of an omaha devserver that we just created for the purposes
1413                     of updating.
1414      devserver_hostname: At the start of each test, we choose a devserver
1415                          machine in the lab for the test. We use the devserver
1416                          instance there (access by autotest_devserver) to stage
1417                          artifacts. However, we also use the same host to start
1418                          omaha devserver instances for updating machines with
1419                          (that reference the staged paylaods on the autotest
1420                          devserver instance). This hostname refers to that
1421                          machine we are using (since it's always the same for
1422                          both staging/omaha'ing).
1423
1424    """
1425    version = 1
1426
1427    # Timeout periods, given in seconds.
1428    _WAIT_AFTER_SHUTDOWN_SECONDS = 10
1429    _WAIT_AFTER_UPDATE_SECONDS = 20
1430    _WAIT_FOR_USB_INSTALL_SECONDS = 4 * 60
1431    _WAIT_FOR_MP_RECOVERY_SECONDS = 8 * 60
1432    _WAIT_FOR_INITIAL_UPDATE_CHECK_SECONDS = 12 * 60
1433    # TODO(sosa): Investigate why this needs to be so long (this used to be
1434    # 120 and regressed).
1435    _WAIT_FOR_DOWNLOAD_STARTED_SECONDS = 4 * 60
1436    _WAIT_FOR_DOWNLOAD_COMPLETED_SECONDS = 10 * 60
1437    _WAIT_FOR_UPDATE_COMPLETED_SECONDS = 4 * 60
1438    _WAIT_FOR_UPDATE_CHECK_AFTER_REBOOT_SECONDS = 15 * 60
1439    _DEVSERVER_HOSTLOG_REQUEST_TIMEOUT_SECONDS = 30
1440
1441    # Logs and their whereabouts.
1442    _WHERE_UPDATE_LOG = ('update_engine log (in sysinfo or on the DUT, also '
1443                         'included in the test log)')
1444    _WHERE_OMAHA_LOG = 'Omaha-devserver log (included in the test log)'
1445
1446
1447    def initialize(self):
1448        """Sets up variables that will be used by test."""
1449        self._host = None
1450        self._omaha_devserver = None
1451        self._source_image_installed = False
1452
1453        self._devserver_dir = global_config.global_config.get_config_value(
1454                'CROS', 'devserver_dir', default=None)
1455        if self._devserver_dir is None:
1456            raise error.TestError(
1457                    'Path to devserver source tree not provided; please define '
1458                    'devserver_dir under [CROS] in your shadow_config.ini')
1459
1460
1461    def cleanup(self):
1462        """Kill the omaha devserver if it's still around."""
1463        if self._omaha_devserver:
1464            self._omaha_devserver.stop_devserver()
1465
1466        self._omaha_devserver = None
1467
1468
1469    def _dump_update_engine_log(self, test_platform):
1470        """Dumps relevant AU error log."""
1471        try:
1472            error_log = test_platform.get_update_log(80)
1473            logging.error('Dumping snippet of update_engine log:\n%s',
1474                          snippet(error_log))
1475        except Exception:
1476            # Mute any exceptions we get printing debug logs.
1477            pass
1478
1479
1480    def _report_perf_data(self, perf_data):
1481        """Reports performance and resource data.
1482
1483        Currently, performance attributes are expected to include 'rss_peak'
1484        (peak memory usage in bytes).
1485
1486        @param perf_data: A dictionary containing performance attributes.
1487        """
1488        rss_peak = perf_data.get('rss_peak')
1489        if rss_peak:
1490            rss_peak_kib = rss_peak / 1024
1491            logging.info('Peak memory (RSS) usage on DUT: %d KiB', rss_peak_kib)
1492            self.output_perf_value(description='mem_usage_peak',
1493                                   value=int(rss_peak_kib),
1494                                   units='KiB',
1495                                   higher_is_better=False)
1496        else:
1497            logging.warning('No rss_peak key in JSON returned by '
1498                            'update_engine_performance_monitor.py')
1499
1500
1501    def _error_initial_check(self, expected, actual, mismatched_attrs):
1502        if 'version' in mismatched_attrs:
1503            err_msg = ('Initial update check was received but the reported '
1504                       'version is different from what was expected.')
1505            if self._source_image_installed:
1506                err_msg += (' The source payload we installed was probably '
1507                            'incorrect or corrupt.')
1508            else:
1509                err_msg += (' The DUT is probably not running the correct '
1510                            'source image.')
1511            return err_msg
1512
1513        return 'A test bug occurred; inspect the test log.'
1514
1515
1516    def _error_intermediate(self, expected, actual, mismatched_attrs, action,
1517                            problem):
1518        if 'event_result' in mismatched_attrs:
1519            event_result = actual.get('event_result')
1520            reported = (('different than expected (%s)' %
1521                         EVENT_RESULT_DICT[event_result])
1522                        if event_result else 'missing')
1523            return ('The updater reported result code is %s. This could be an '
1524                    'updater bug or a connectivity problem; check the %s. For '
1525                    'a detailed log of update events, check the %s.' %
1526                    (reported, self._WHERE_UPDATE_LOG, self._WHERE_OMAHA_LOG))
1527        if 'event_type' in mismatched_attrs:
1528            event_type = actual.get('event_type')
1529            reported = ('different (%s)' % EVENT_TYPE_DICT[event_type]
1530                        if event_type else 'missing')
1531            return ('Expected the updater to %s (%s) but received event type '
1532                    'is %s. This could be an updater %s; check the '
1533                    '%s. For a detailed log of update events, check the %s.' %
1534                    (action, EVENT_TYPE_DICT[expected['event_type']], reported,
1535                     problem, self._WHERE_UPDATE_LOG, self._WHERE_OMAHA_LOG))
1536        if 'version' in mismatched_attrs:
1537            return ('The updater reported an unexpected version despite '
1538                    'previously reporting the correct one. This is most likely '
1539                    'a bug in update engine; check the %s.' %
1540                    self._WHERE_UPDATE_LOG)
1541
1542        return 'A test bug occurred; inspect the test log.'
1543
1544
1545    def _error_download_started(self, expected, actual, mismatched_attrs):
1546        return self._error_intermediate(expected, actual, mismatched_attrs,
1547                                        'begin downloading',
1548                                        'bug, crash or provisioning error')
1549
1550
1551    def _error_download_finished(self, expected, actual, mismatched_attrs):
1552        return self._error_intermediate(expected, actual, mismatched_attrs,
1553                                        'finish downloading', 'bug or crash')
1554
1555
1556    def _error_update_complete(self, expected, actual, mismatched_attrs):
1557        return self._error_intermediate(expected, actual, mismatched_attrs,
1558                                        'complete the update', 'bug or crash')
1559
1560
1561    def _error_reboot_after_update(self, expected, actual, mismatched_attrs):
1562        if 'event_result' in mismatched_attrs:
1563            event_result = actual.get('event_result')
1564            reported = ('different (%s)' % EVENT_RESULT_DICT[event_result]
1565                        if event_result else 'missing')
1566            return ('The updater was expected to reboot (%s) but reported '
1567                    'result code is %s. This could be a failure to reboot, an '
1568                    'updater bug or a connectivity problem; check the %s and '
1569                    'the system log. For a detailed log of update events, '
1570                    'check the %s.' %
1571                    (EVENT_RESULT_DICT[expected['event_result']], reported,
1572                     self._WHERE_UPDATE_LOG, self._WHERE_OMAHA_LOG))
1573        if 'event_type' in mismatched_attrs:
1574            event_type = actual.get('event_type')
1575            reported = ('different (%s)' % EVENT_TYPE_DICT[event_type]
1576                        if event_type else 'missing')
1577            return ('Expected to successfully reboot into the new image (%s) '
1578                    'but received event type is %s. This probably means that '
1579                    'the new image failed to verify after reboot, possibly '
1580                    'because the payload is corrupt. This might also be an '
1581                    'updater bug or crash; check the %s. For a detailed log of '
1582                    'update events, check the %s.' %
1583                    (EVENT_TYPE_DICT[expected['event_type']], reported,
1584                     self._WHERE_UPDATE_LOG, self._WHERE_OMAHA_LOG))
1585        if 'version' in mismatched_attrs:
1586            return ('The DUT rebooted after the update but reports a different '
1587                    'image version than the one expected. This probably means '
1588                    'that the payload we applied was incorrect or corrupt.')
1589        if 'previous_version' in mismatched_attrs:
1590            return ('The DUT rebooted after the update and reports the '
1591                    'expected version. However, it reports a previous version '
1592                    'that is different from the one previously reported. This '
1593                    'is most likely a bug in update engine; check the %s.' %
1594                    self._WHERE_UPDATE_LOG)
1595
1596        return 'A test bug occurred; inspect the test log.'
1597
1598
1599    def _timeout_err(self, desc, timeout, event_type=None):
1600        if event_type is not None:
1601            desc += ' (%s)' % EVENT_TYPE_DICT[event_type]
1602        return ('Failed to receive %s within %d seconds. This could be a '
1603                'problem with the updater or a connectivity issue. For more '
1604                'details, check the %s.' %
1605                (desc, timeout, self._WHERE_UPDATE_LOG))
1606
1607
1608    def run_update_test(self, test_platform, test_conf):
1609        """Runs the actual update test once preconditions are met.
1610
1611        @param test_platform: TestPlatform implementation.
1612        @param test_conf: A dictionary containing test configuration values
1613
1614        @raises ExpectedUpdateEventChainFailed if we failed to verify an update
1615                event.
1616        """
1617
1618        # Record the active root partition.
1619        source_active_slot = test_platform.get_active_slot()
1620        logging.info('Source active slot: %s', source_active_slot)
1621
1622        source_release = test_conf['source_release']
1623        target_release = test_conf['target_release']
1624
1625        # Start the performance monitoring process on the DUT.
1626        test_platform.start_update_perf(self.bindir)
1627        try:
1628            # Trigger an update.
1629            test_platform.trigger_update(self._omaha_devserver)
1630
1631            # Track update progress.
1632            omaha_netloc = self._omaha_devserver.get_netloc()
1633            omaha_hostlog_url = urlparse.urlunsplit(
1634                    ['http', omaha_netloc, '/api/hostlog',
1635                     'ip=' + self._host.ip, ''])
1636            logging.info('Polling update progress from omaha/devserver: %s',
1637                         omaha_hostlog_url)
1638            log_verifier = UpdateEventLogVerifier(
1639                    omaha_hostlog_url,
1640                    self._DEVSERVER_HOSTLOG_REQUEST_TIMEOUT_SECONDS)
1641
1642            # Verify chain of events in a successful update process.
1643            chain = ExpectedUpdateEventChain()
1644            chain.add_event(
1645                    ExpectedUpdateEvent(
1646                        version=source_release,
1647                        on_error=self._error_initial_check),
1648                    self._WAIT_FOR_INITIAL_UPDATE_CHECK_SECONDS,
1649                    on_timeout=self._timeout_err(
1650                            'an initial update check',
1651                            self._WAIT_FOR_INITIAL_UPDATE_CHECK_SECONDS))
1652            chain.add_event(
1653                    ExpectedUpdateEvent(
1654                        event_type=EVENT_TYPE_DOWNLOAD_STARTED,
1655                        event_result=EVENT_RESULT_SUCCESS,
1656                        version=source_release,
1657                        on_error=self._error_download_started),
1658                    self._WAIT_FOR_DOWNLOAD_STARTED_SECONDS,
1659                    on_timeout=self._timeout_err(
1660                            'a download started notification',
1661                            self._WAIT_FOR_DOWNLOAD_STARTED_SECONDS,
1662                            event_type=EVENT_TYPE_DOWNLOAD_STARTED))
1663            chain.add_event(
1664                    ExpectedUpdateEvent(
1665                        event_type=EVENT_TYPE_DOWNLOAD_FINISHED,
1666                        event_result=EVENT_RESULT_SUCCESS,
1667                        version=source_release,
1668                        on_error=self._error_download_finished),
1669                    self._WAIT_FOR_DOWNLOAD_COMPLETED_SECONDS,
1670                    on_timeout=self._timeout_err(
1671                            'a download finished notification',
1672                            self._WAIT_FOR_DOWNLOAD_COMPLETED_SECONDS,
1673                            event_type=EVENT_TYPE_DOWNLOAD_FINISHED))
1674            chain.add_event(
1675                    ExpectedUpdateEvent(
1676                        event_type=EVENT_TYPE_UPDATE_COMPLETE,
1677                        event_result=EVENT_RESULT_SUCCESS,
1678                        version=source_release,
1679                        on_error=self._error_update_complete),
1680                    self._WAIT_FOR_UPDATE_COMPLETED_SECONDS,
1681                    on_timeout=self._timeout_err(
1682                            'an update complete notification',
1683                            self._WAIT_FOR_UPDATE_COMPLETED_SECONDS,
1684                            event_type=EVENT_TYPE_UPDATE_COMPLETE))
1685
1686            log_verifier.verify_expected_events_chain(chain)
1687
1688            # Wait after an update completion (safety margin).
1689            _wait(self._WAIT_AFTER_UPDATE_SECONDS, 'after update completion')
1690        finally:
1691            # Terminate perf monitoring process and collect its output.
1692            perf_data = test_platform.stop_update_perf()
1693            if perf_data:
1694                self._report_perf_data(perf_data)
1695
1696        # Only update the stateful partition (the test updated the rootfs).
1697        test_platform.finalize_update()
1698
1699        # Reboot the DUT after the update.
1700        test_platform.reboot_device()
1701
1702        # Trigger a second update check (again, test vs MP).
1703        test_platform.trigger_update(self._omaha_devserver)
1704
1705        # Observe post-reboot update check, which should indicate that the
1706        # image version has been updated.
1707        chain = ExpectedUpdateEventChain()
1708        expected_events = [
1709            ExpectedUpdateEvent(
1710                event_type=EVENT_TYPE_UPDATE_COMPLETE,
1711                event_result=EVENT_RESULT_SUCCESS_REBOOT,
1712                version=target_release,
1713                previous_version=source_release,
1714                on_error=self._error_reboot_after_update),
1715            # Newer versions send a "rebooted_after_update" message after reboot
1716            # with the previous version instead of another "update_complete".
1717            ExpectedUpdateEvent(
1718                event_type=EVENT_TYPE_REBOOTED_AFTER_UPDATE,
1719                event_result=EVENT_RESULT_SUCCESS,
1720                version=target_release,
1721                previous_version=source_release,
1722                on_error=self._error_reboot_after_update),
1723        ]
1724        chain.add_event(
1725                expected_events,
1726                self._WAIT_FOR_UPDATE_CHECK_AFTER_REBOOT_SECONDS,
1727                on_timeout=self._timeout_err(
1728                        'a successful reboot notification',
1729                        self._WAIT_FOR_UPDATE_CHECK_AFTER_REBOOT_SECONDS,
1730                        event_type=EVENT_TYPE_UPDATE_COMPLETE))
1731
1732        log_verifier.verify_expected_events_chain(chain)
1733
1734        # Make sure we're using a different slot after the update.
1735        target_active_slot = test_platform.get_active_slot()
1736        if target_active_slot == source_active_slot:
1737            err_msg = 'The active image slot did not change after the update.'
1738            if None in (source_release, target_release):
1739                err_msg += (' The DUT likely rebooted into the old image, which '
1740                            'probably means that the payload we applied was '
1741                            'corrupt. But since we did not check the source '
1742                            'and/or target version we cannot say for sure.')
1743            elif source_release == target_release:
1744                err_msg += (' Given that the source and target versions are '
1745                            'identical, the DUT likely rebooted into the old '
1746                            'image. This probably means that the payload we '
1747                            'applied was corrupt.')
1748            else:
1749                err_msg += (' This is strange since the DUT reported the '
1750                            'correct target version. This is probably a system '
1751                            'bug; check the DUT system log.')
1752            raise error.TestFail(err_msg)
1753
1754        logging.info('Target active slot changed as expected: %s',
1755                     target_active_slot)
1756
1757        logging.info('Update successful, test completed')
1758
1759
1760    # TODO(garnold) Remove the use_servo argument once control files on all
1761    # release branches have caught up.
1762    def run_once(self, host, test_conf, use_servo=False):
1763        """Performs a complete auto update test.
1764
1765        @param host: a host object representing the DUT
1766        @param test_conf: a dictionary containing test configuration values
1767        @param use_servo: DEPRECATED
1768
1769        @raise error.TestError if anything went wrong with setting up the test;
1770               error.TestFail if any part of the test has failed.
1771
1772        """
1773
1774        self._host = host
1775
1776        # Find a devserver to use. We first try to pick a devserver with the
1777        # least load. In case all devservers' load are higher than threshold,
1778        # fall back to the old behavior by picking a devserver based on the
1779        # payload URI, with which ImageServer.resolve will return a random
1780        # devserver based on the hash of the URI.
1781        # The picked devserver needs to respect the location of the host if
1782        # `prefer_local_devserver` is set to True or `restricted_subnets` is
1783        # set.
1784        hostname = self._host.hostname if self._host else None
1785        least_loaded_devserver = dev_server.get_least_loaded_devserver(
1786                hostname=hostname)
1787        if least_loaded_devserver:
1788            logging.debug('Choose the least loaded devserver: %s',
1789                          least_loaded_devserver)
1790            autotest_devserver = dev_server.ImageServer(least_loaded_devserver)
1791        else:
1792            logging.warning('No devserver meets the maximum load requirement. '
1793                            'Pick a random devserver to use.')
1794            autotest_devserver = dev_server.ImageServer.resolve(
1795                    test_conf['target_payload_uri'], host.hostname)
1796        devserver_hostname = urlparse.urlparse(
1797                autotest_devserver.url()).hostname
1798
1799        # Obtain a test platform implementation.
1800        test_platform = TestPlatform.create(host)
1801        test_platform.initialize(autotest_devserver, self._devserver_dir)
1802
1803        # Stage source images and update payloads onto a devserver.
1804        staged_urls = test_platform.prep_artifacts(test_conf)
1805        self._source_image_installed = bool(staged_urls.source_url)
1806
1807        # Prepare the DUT (install source version etc).
1808        test_platform.prep_device_for_update(test_conf['source_release'])
1809
1810        self._omaha_devserver = OmahaDevserver(
1811                devserver_hostname, self._devserver_dir,
1812                staged_urls.target_url)
1813        self._omaha_devserver.start_devserver()
1814
1815        try:
1816            self.run_update_test(test_platform, test_conf)
1817        except ExpectedUpdateEventChainFailed:
1818            self._dump_update_engine_log(test_platform)
1819            raise
1820
1821        test_platform.check_device_after_update(test_conf['target_release'])
1822