• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2015 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 json
6import logging
7import os
8import time
9
10from autotest_lib.client.common_lib.cros import arc
11
12from autotest_lib.client.common_lib.cros.arc import is_android_container_alive
13
14from autotest_lib.client.bin import test
15from autotest_lib.client.common_lib import error
16from autotest_lib.client.common_lib.cros import chrome
17from autotest_lib.client.common_lib.cros import enrollment
18from autotest_lib.client.common_lib.cros import policy
19from autotest_lib.client.cros import cryptohome
20from autotest_lib.client.cros import httpd
21from autotest_lib.client.cros.input_playback import keyboard
22from autotest_lib.client.cros.enterprise import enterprise_policy_utils
23from autotest_lib.client.cros.enterprise import policy_manager
24from autotest_lib.client.cros.enterprise import enterprise_fake_dmserver
25from autotest_lib.client.common_lib import ui_utils
26
27from telemetry.core import exceptions
28
29CROSQA_FLAGS = [
30    '--gaia-url=https://gaiastaging.corp.google.com',
31    '--lso-url=https://gaiastaging.corp.google.com',
32    '--google-apis-url=https://www-googleapis-test.sandbox.google.com',
33    '--oauth2-client-id=236834563817.apps.googleusercontent.com',
34    '--oauth2-client-secret=RsKv5AwFKSzNgE0yjnurkPVI',
35    ('--cloud-print-url='
36     'https://cloudprint-nightly-ps.sandbox.google.com/cloudprint'),
37    '--ignore-urlfetcher-cert-requests']
38CROSALPHA_FLAGS = [
39    ('--cloud-print-url='
40     'https://cloudprint-nightly-ps.sandbox.google.com/cloudprint'),
41    '--ignore-urlfetcher-cert-requests']
42TESTDMS_FLAGS = [
43    '--ignore-urlfetcher-cert-requests',
44    '--disable-policy-key-verification']
45FLAGS_DICT = {
46    'prod': [],
47    'crosman-qa': CROSQA_FLAGS,
48    'crosman-alpha': CROSALPHA_FLAGS,
49    'dm-test': TESTDMS_FLAGS,
50    'dm-fake': TESTDMS_FLAGS
51}
52DMS_URL_DICT = {
53    'prod': 'http://m.google.com/devicemanagement/data/api',
54    'crosman-qa':
55        'https://crosman-qa.sandbox.google.com/devicemanagement/data/api',
56    'crosman-alpha':
57        'https://crosman-alpha.sandbox.google.com/devicemanagement/data/api',
58    'dm-test': 'http://chromium-dm-test.appspot.com/d/%s',
59    'dm-fake': 'http://127.0.0.1:%d/'
60}
61DMSERVER = '--device-management-url=%s'
62# Username and password for the fake dm server can be anything, since
63# they are not used to authenticate against GAIA.
64USERNAME = 'fake-user@managedchrome.com'
65PASSWORD = 'fakepassword'
66GAIA_ID = 'fake-gaia-id'
67
68
69class EnterprisePolicyTest(arc.ArcTest, test.test):
70    """Base class for Enterprise Policy Tests."""
71
72    WEB_PORT = 8080
73    WEB_HOST = 'http://localhost:%d' % WEB_PORT
74    CHROME_POLICY_PAGE = 'chrome://policy'
75    CHROME_VERSION_PAGE = 'chrome://version'
76
77    def initialize(self, **kwargs):
78        """
79        Initialize test parameters.
80
81        Consume the check_client_result parameter if this test was started
82        from a server test.
83
84        """
85        self._initialize_enterprise_policy_test(**kwargs)
86
87    def _initialize_enterprise_policy_test(
88            self, env='dm-fake', dms_name=None,
89            username=USERNAME, password=PASSWORD, gaia_id=GAIA_ID,
90            set_auto_logout=None, **kwargs):
91        """
92        Initialize test parameters and fake DM Server.
93
94        This function exists so that ARC++ tests (which inherit from the
95        ArcTest class) can also initialize a policy setup.
96
97        @param env: String environment of DMS and Gaia servers.
98            If wanting to point to a 'real' server use 'dm-test'
99        @param username: String user name login credential.
100        @param password: String password login credential.
101        @param gaia_id: String gaia_id login credential.
102        @param dms_name: String name of test DM Server.
103        @param kwargs: Not used.
104
105        """
106        self.env = env
107        self.username = username
108        self.password = password
109        self.gaia_id = gaia_id
110        self.set_auto_logout = set_auto_logout
111        self.dms_name = dms_name
112        self.dms_is_fake = (env == 'dm-fake')
113        self.arc_enabled = False
114        self._enforce_variable_restrictions()
115
116        # Install protobufs and add import path.
117        policy.install_protobufs(self.autodir, self.job)
118
119        # Initialize later variables to prevent error after an early failure.
120        self._web_server = None
121        self.cr = None
122
123        # Start AutoTest DM Server if using local fake server.
124        if self.dms_is_fake:
125            self.fake_dm_server = enterprise_fake_dmserver.FakeDMServer()
126            self.fake_dm_server.start(self.tmpdir, self.debugdir)
127
128        # Get enterprise directory of shared resources.
129        client_dir = os.path.dirname(os.path.dirname(self.bindir))
130        self.enterprise_dir = os.path.join(client_dir, 'cros/enterprise')
131
132        if self.set_auto_logout is not None:
133            self._auto_logout = self.set_auto_logout
134
135        # Log the test context parameters.
136        logging.info('Test Context Parameters:')
137        logging.info('  Environment: %r', self.env)
138        logging.info('  Username: %r', self.username)
139        logging.info('  Password: %r', self.password)
140        logging.info('  Test DMS Name: %r', self.dms_name)
141
142    def check_page_readiness(self, tab, command):
143        """
144        Check to see if page has fully loaded.
145
146        @param tab: chrome tab loading the page.
147        @param command: JS command to be checked in the tab.
148
149        @returns True if loaded and False if not.
150
151        """
152        try:
153            tab.EvaluateJavaScript(command)
154            return True
155        except exceptions.EvaluateException:
156            return False
157
158    def cleanup(self):
159        """Close out anything used by this test."""
160        # Clean up AutoTest DM Server if using local fake server.
161        if self.dms_is_fake:
162            self.fake_dm_server.stop()
163
164        # Stop web server if it was started.
165        if self._web_server:
166            self._web_server.stop()
167
168        # Close Chrome instance if opened.
169        if self.cr and self._auto_logout:
170            self.cr.close()
171
172        # Cleanup the ARC test if needed.
173        if self.arc_enabled:
174            super(EnterprisePolicyTest, self).cleanup()
175
176    def start_webserver(self):
177        """Set up HTTP Server to serve pages from enterprise directory."""
178        self._web_server = httpd.HTTPListener(
179            self.WEB_PORT, docroot=self.enterprise_dir)
180        self._web_server.run()
181
182    def _enforce_variable_restrictions(self):
183        """Validate class-level test context parameters.
184
185        @raises error.TestError if context parameter has an invalid value,
186                or a combination of parameters have incompatible values.
187        """
188        # Verify |env| is a valid environment.
189        if self.env not in FLAGS_DICT:
190            raise error.TestError('Environment is invalid: %s' % self.env)
191
192        # Verify test |dms_name| is given if |env| is 'dm-test'.
193        if self.env == 'dm-test' and not self.dms_name:
194            raise error.TestError('dms_name must be given when using '
195                                  'env=dm-test.')
196        if self.env != 'dm-test' and self.dms_name:
197            raise error.TestError('dms_name must not be given when not using '
198                                  'env=dm-test.')
199        if self.env == 'prod' and self.username == USERNAME:
200            raise error.TestError('Cannot user real DMS server when using '
201                                  'a fake test account.')
202
203    def setup_case(self,
204                   user_policies={},
205                   suggested_user_policies={},
206                   device_policies={},
207                   extension_policies={},
208                   skip_policy_value_verification=False,
209                   kiosk_mode=False,
210                   enroll=False,
211                   auto_login=True,
212                   auto_logout=True,
213                   init_network_controller=False,
214                   arc_mode=None,
215                   setup_arc=True,
216                   use_clouddpc_test=None,
217                   disable_default_apps=True,
218                   real_gaia=False,
219                   extension_paths=[],
220                   extra_chrome_flags=[]):
221        """Set up DMS, log in, and verify policy values.
222
223        If the AutoTest fake DM Server is used, make a JSON policy blob
224        and upload it to the fake DM server.
225
226        Launch Chrome and sign in to Chrome OS. Examine the user's
227        cryptohome vault, to confirm user is signed in successfully.
228
229        @param user_policies: dict of mandatory user policies in
230                name -> value format.
231        @param suggested_user_policies: optional dict of suggested policies
232                in name -> value format.
233        @param device_policies: dict of device policies in
234                name -> value format.
235        @param extension_policies: dict of extension policies.
236        @param skip_policy_value_verification: True if setup_case should not
237                verify that the correct policy value shows on policy page.
238        @param enroll: True for enrollment instead of login.
239        @param auto_login: Sign in to chromeos.
240        @param auto_logout: Sign out of chromeos when test is complete.
241        @param init_network_controller: whether to init network controller.
242        @param arc_mode: whether to enable arc_mode on chrome.chrome().
243        @param setup_arc: whether to run setup_arc in arc.Arctest.
244        @param use_clouddpc_test: whether to run the cloud dpc test.
245        @param extension_paths: list of extensions to install.
246        @param extra_chrome_flags: list of flags to add to Chrome.
247
248        @raises error.TestError if cryptohome vault is not mounted for user.
249        @raises error.TestFail if |policy_name| and |policy_value| are not
250                shown on the Policies page.
251        """
252
253        # Need a real account, for now. Note: Even though the account is 'real'
254        # you can still use a fake DM server.
255
256        if (arc_mode and self.username == USERNAME) or real_gaia:
257            self.username = 'tester50@managedchrome.com'
258            self.password = 'Test0000'
259
260        self.pol_manager = policy_manager.Policy_Manager(
261            self.username,
262            self.fake_dm_server if hasattr(self, "fake_dm_server") else None)
263
264        self._auto_logout = auto_logout
265        self._kiosk_mode = kiosk_mode
266
267        self.pol_manager.configure_policies(user_policies,
268                                            suggested_user_policies,
269                                            device_policies,
270                                            extension_policies)
271
272        self._create_chrome(enroll=enroll,
273                            auto_login=auto_login,
274                            init_network_controller=init_network_controller,
275                            extension_paths=extension_paths,
276                            arc_mode=arc_mode,
277                            disable_default_apps=disable_default_apps,
278                            real_gaia=real_gaia,
279                            extra_chrome_flags=extra_chrome_flags)
280
281        self.pol_manager.obtain_policies_from_device(self.cr.autotest_ext)
282
283        # Skip policy check upon request or if we enroll but don't log in.
284        if not skip_policy_value_verification and auto_login:
285            self.pol_manager.verify_policies()
286
287        if arc_mode:
288            self.start_arc(use_clouddpc_test, setup_arc)
289
290    def update_DM_Info(self):
291        """Force an update of the DM server from current policies."""
292        if not self.pol_manager.fake_dm_server:
293            raise error.TestError('Cannot autoupdate DM sever without the fake'
294                                  ' DM Sever being configured')
295        self.pol_manager.updateDMServer()
296
297    def enable_Fake_DM_autoupdate(self):
298        """Enable automatic DM server updates on policy change."""
299        self.pol_manager.auto_updateDM = True
300
301    def disable_Fake_DM_autoupdate(self):
302        """Disable automatic DM server updates on policy change."""
303        self.pol_manager.auto_updateDM = False
304
305    def remove_policy(self,
306                      name,
307                      policy_type,
308                      extID=None,
309                      verify_policies=True):
310        """
311        Remove a policy from the currently configured policies.
312
313        @param name: str, the name of the policy
314        @param policy_type: str, the policy type. Must "extension" for
315            extension policies, other for user/device/suggested
316        @param ExtID: str, ID of extension to remove the policy from, if
317            policy_type is extension
318        @param verify_policies: bool, re-verify policies after removal of the
319            specified policy
320
321        """
322        self.pol_manager.remove_policy(name, policy_type, extID)
323        self.reload_policies(verify_policies)
324        if verify_policies:
325            self.pol_manager.verify_policies()
326
327    def start_arc(self, use_clouddpc_test, setup_arc):
328        """
329        Start ARC when creating the chrome object.
330
331        Specifically will create the ADB shell container for testing use.
332
333        We are NOT going to use the arc class initialize, it overwrites the
334        creation of chrome.chrome() in a way which cannot support the DM sever.
335
336        Instead we check for the android container, and run arc_setup if
337        needed. Note: To use the cloud dpc test, you MUST also run setup_arc
338
339        @param setup_arc: whether to run setup_arc in arc.Arctest.
340        @param use_clouddpc_test: bool, run_clouddpc_test() or not.
341
342        """
343        _APP_FILENAME = 'autotest-deps-cloudpctest-0.4.apk'
344        _DEP_PACKAGE = 'CloudDPCTest-apks'
345        _PKG_NAME = 'com.google.android.apps.work.clouddpc.e2etests'
346
347        # By default on boot the container is alive, and will not close until
348        # a user with ARC disabled logs in. This wait accounts for that.
349        time.sleep(3)
350
351        if use_clouddpc_test and not setup_arc:
352            raise error.TestFail('For cloud DPC setup_arc cannot be disabled')
353
354        if is_android_container_alive():
355            logging.info('Android Container is alive!')
356        else:
357            logging.error('Android Container is not alive!')
358
359        # Install the clouddpc test.
360        if use_clouddpc_test:
361            self.arc_setup(dep_packages=_DEP_PACKAGE,
362                           apks=[_APP_FILENAME],
363                           full_pkg_names=[_PKG_NAME])
364            self.run_clouddpc_test()
365        else:
366            if setup_arc:
367                self.arc_setup()
368
369        self.arc_enabled = True
370
371    def run_clouddpc_test(self):
372        """
373        Run clouddpc end-to-end test and fail this test if it fails.
374
375        Assumes start_arc() was run with use_clouddpc_test.
376
377        Determines the policy values to pass to the test from those set in
378        Chrome OS.
379
380        @raises error.TestFail if the test does not pass.
381
382        """
383        policy_blob = self.pol_manager.getCloudDpc()
384        policy_blob_str = json.dumps(policy_blob, separators=(',', ':'))
385        cmd = ('am instrument -w -e policy "%s" '
386               'com.google.android.apps.work.clouddpc.e2etests/'
387               '.ArcInstrumentationTestRunner') % policy_blob_str
388
389        # Run the command as a shell script so that its length is not capped.
390        temp_shell_script_path = '/sdcard/tmp.sh'
391        arc.write_android_file(temp_shell_script_path, cmd)
392
393        logging.info('Running clouddpc test with policy: %s', policy_blob_str)
394        results = arc.adb_shell('sh ' + temp_shell_script_path,
395                                ignore_status=True).strip()
396        arc.remove_android_file(temp_shell_script_path)
397        if results.find('FAILURES!!!') >= 0:
398            logging.info('CloudDPC E2E Results:\n%s', results)
399            err_msg = results.splitlines()[-1]
400            raise error.TestFail('CloudDPC E2E failure: %s' % err_msg)
401
402        logging.debug(results)
403        logging.info('CloudDPC E2E test passed!')
404
405    def _get_policy_value_from_new_tab(self, policy):
406        """
407        TO BE DEPRICATED DO NOT USE.
408
409        Gets the value of the policy.
410
411        """
412        return self.pol_manager.get_policy_value_from_DUT(policy)
413
414    def add_policies(self,
415                     user={},
416                     suggested_user={},
417                     device={},
418                     extension={},
419                     new=False):
420        """Add policies to the policy rules."""
421        self.pol_manager.configure_policies(user=user,
422                                            suggested_user=suggested_user,
423                                            device=device,
424                                            extension=extension,
425                                            new=new)
426        self.reload_policies(True)
427
428    def update_policies(self, user_policies={}, suggested_user_policies={},
429                        device_policies={}, extension_policies={}):
430        """
431        Change the policies to the ones provided.
432
433        NOTE: This will override any current policies configured.
434
435        @param user_policies: mandatory user policies -> values.
436        @param suggested user_policies: suggested user policies -> values.
437        @param device_policies: mandatory device policies -> values.
438        @param extension_policies: extension policies.
439
440        """
441        self.add_policies(user_policies, suggested_user_policies,
442                          device_policies, extension_policies, True)
443
444    def reload_policies(self, wait_for_new=False):
445        """
446        Force a policy fetch.
447
448        @param wait_for_new: bool, wait up to 1 second for the policy values
449            from the API to update
450
451        """
452        enterprise_policy_utils.refresh_policies(self.cr.autotest_ext,
453                                                 wait_for_new)
454
455    def verify_extension_stats(self, extension_policies, sensitive_fields=[]):
456        """
457        Verify the extension policies match what is on chrome://policy.
458
459        @param extension_policies: the dictionary of extension IDs mapping
460            to download_url and secure_hash.
461        @param sensitive_fields: list of fields that should have their value
462            censored.
463        @raises error.TestError: if the shown values do not match what we are
464            expecting.
465
466        """
467        # Refetch the policies from the DUT
468        self.pol_manager.obtain_policies_from_device()
469
470        # For this method we are expecting the extension policies to be living
471        # within the chrome file system.
472        for ext_id in extension_policies.keys():
473            filePath = extension_policies[ext_id]['download_url']
474            actual_extension_policies = (
475                self._get_extension_policies_from_file(filePath))
476
477            # Obfuscate and strip the extra dict level
478            self._configure_extension_file_policies(actual_extension_policies,
479                                                    sensitive_fields)
480
481            self.pol_manager.configure_extension_visual_policy(
482                {ext_id: actual_extension_policies})
483
484            self.pol_manager.verify_policies()
485
486    def _configure_extension_file_policies(self, policies, sensitive_fields):
487        """
488        Given policies, change sensitive_fields to "********".
489
490        @param policies: dict, the extension policies as loaded from the file.
491        @param sensitive_fields: list or set
492
493        """
494        for policy_name, value in policies.items():
495            if policy_name in sensitive_fields:
496                policies[policy_name] = '*' * 8
497            else:
498                if 'Value' in value:
499                    policies[policy_name] = value['Value']
500
501    def _get_extension_policies_from_file(self, download_url):
502        """
503        Get the configured extension policies from file system.
504
505        Extension tests will store the "actual" extensions as a file within
506        ChromeOS. Open the file within the enterprise_dir, and return the
507        policies from the file.
508
509        @param download_url: URL of the download location
510
511        """
512        policy_file = os.path.join(self.enterprise_dir,
513                                   download_url.split('/')[-1])
514
515        with open(policy_file) as f:
516            policies = json.load(f)
517        return policies
518
519    def verify_policy_value(self,
520                            policy_name,
521                            expected_value,
522                            extensionID=None):
523        """
524        Verify the value of a single policy.
525
526        Note: This will not format the expected value. (Ie it will not
527        automatically change a password/sensitive field to ********)
528
529        @param policy_name: the policy we are checking.
530        @param expected_value: the expected value for policy_name.
531
532        @raises error.TestError if value does not match expected.
533
534        """
535        self.pol_manager.verify_policy(policy_name,
536                                       expected_value,
537                                       extensionID)
538
539    def _initialize_chrome_extra_flags(self):
540        """
541        Initialize flags used to create Chrome instance.
542
543        @returns: list of extra Chrome flags.
544
545        """
546        # Construct DM Server URL flags if not using production server.
547        env_flag_list = []
548        if self.env != 'prod':
549            if self.dms_is_fake:
550                # Use URL provided by the fake AutoTest DM server.
551                dmserver_str = (DMSERVER % self.fake_dm_server.server_url)
552            else:
553                # Use URL defined in the DMS URL dictionary.
554                dmserver_str = (DMSERVER % (DMS_URL_DICT[self.env]))
555                if self.env == 'dm-test':
556                    dmserver_str = (dmserver_str % self.dms_name)
557
558            # Merge with other flags needed by non-prod enviornment.
559            env_flag_list = ([dmserver_str] + FLAGS_DICT[self.env])
560
561        return env_flag_list
562
563    def _enterprise_enroll(self, extra_flags, extension_paths, auto_login):
564        """
565        Enroll a device, configure self.cr, optionally login.
566
567        @param extra_chrome_flags: list of flags to add.
568        @param extension_paths: list of extensions to install.
569        @param auto_login: sign in to chromeos.
570
571        """
572        self.cr = chrome.Chrome(
573            auto_login=False,
574            autotest_ext=True,
575            extra_browser_args=extra_flags,
576            extension_paths=extension_paths,
577            expect_policy_fetch=True)
578
579        if self.dms_is_fake:
580            if self._kiosk_mode:
581                enrollment.KioskEnrollment(
582                    self.cr.browser, self.username, self.password,
583                    self.gaia_id)
584            else:
585                enrollment.EnterpriseFakeEnrollment(
586                    self.cr.browser, self.username, self.password,
587                    self.gaia_id, auto_login=auto_login)
588        else:
589            enrollment.EnterpriseEnrollment(
590                self.cr.browser, self.username, self.password,
591                auto_login=auto_login)
592
593    def _create_chrome(self,
594                       enroll=False,
595                       auto_login=True,
596                       arc_mode=None,
597                       real_gaia=False,
598                       init_network_controller=False,
599                       disable_default_apps=True,
600                       extension_paths=[],
601                       extra_chrome_flags=[]):
602        """
603        Create a Chrome object. Enroll and/or sign in.
604
605        Function results in self.cr set as the Chrome object.
606
607        @param enroll: enroll the device.
608        @param auto_login: sign in to chromeos.
609        @param arc_mode: enable arc mode.
610        @param real_gaia: to login with a real gaia account or not.
611        @param init_network_controller: whether to init network controller.
612        @param disable_default_apps: disables default apps or not
613            (e.g. the Files app).
614        @param extension_paths: list of extensions to install.
615        @param extra_chrome_flags: list of flags to add.
616        """
617        extra_flags = (self._initialize_chrome_extra_flags() +
618                       extra_chrome_flags)
619
620        logging.info('Chrome Browser Arguments:')
621        logging.info('  extra_browser_args: %s', extra_flags)
622        logging.info('  username: %s', self.username)
623        logging.info('  password: %s', self.password)
624        logging.info('  gaia_login: %s', not self.dms_is_fake)
625        logging.info('  enroll: %s', enroll)
626
627        if enroll:
628            self._enterprise_enroll(extra_flags, extension_paths, auto_login)
629        elif auto_login:
630            # gaia_login (aka real login) is true on arc_mode, or real_gaia.
631            # otherwise: gaia_login is False when a fake dm server is used.
632            gaia_login = (True if (arc_mode or real_gaia)
633                          else not self.dms_is_fake)
634            # Do not disable_gaia_services, or disable_arc_opt_in
635            # if using a gaia_login.
636            disable_gaia_services = disable_arc_opt_in = not gaia_login
637            enterprise_arc_test = gaia_login
638
639            self.cr = chrome.Chrome(
640                extra_browser_args=extra_flags,
641                username=self.username,
642                password=self.password,
643                gaia_login=gaia_login,
644                arc_mode=arc_mode,
645                disable_arc_opt_in=disable_arc_opt_in,
646                disable_gaia_services=disable_gaia_services,
647                autotest_ext=True,
648                enterprise_arc_test=enterprise_arc_test,
649                init_network_controller=init_network_controller,
650                expect_policy_fetch=True,
651                extension_paths=extension_paths,
652                disable_default_apps=disable_default_apps)
653
654        else:
655            self.cr = chrome.Chrome(
656                auto_login=False,
657                extra_browser_args=extra_flags,
658                disable_gaia_services=self.dms_is_fake,
659                autotest_ext=True,
660                expect_policy_fetch=True)
661
662        self.ui = ui_utils.UI_Handler()
663        # Used by arc.py to determine the state of the chrome obj
664        self.initialized = True
665        if auto_login:
666            if not cryptohome.is_vault_mounted(user=self.username,
667                                               allow_fail=True):
668                raise error.TestError('Expected to find a mounted vault for %s.'
669                                      % self.username)
670
671    def navigate_to_url(self, url, tab=None):
672        """Navigate tab to the specified |url|. Create new tab if none given.
673
674        @param url: URL of web page to load.
675        @param tab: browser tab to load (if any).
676        @returns: browser tab loaded with web page.
677        @raises: telemetry TimeoutException if document ready state times out.
678        """
679        logging.info('Navigating to URL: %r', url)
680        if not tab:
681            tab = self.cr.browser.tabs.New()
682            tab.Activate()
683        tab.Navigate(url, timeout=8)
684        tab.WaitForDocumentReadyStateToBeComplete()
685        return tab
686
687    def get_elements_from_page(self, tab, cmd):
688        """Get collection of page elements that match the |cmd| filter.
689
690        @param tab: tab containing the page to be scraped.
691        @param cmd: JavaScript command to evaluate on the page.
692        @returns object containing elements on page that match the cmd.
693        @raises: TestFail if matching elements are not found on the page.
694        """
695        try:
696            elements = tab.EvaluateJavaScript(cmd)
697        except Exception as err:
698            raise error.TestFail('Unable to find matching elements on '
699                                 'the test page: %s\n %r' % (tab.url, err))
700        return elements
701
702    def log_out_via_keyboard(self):
703        """Log out of the device using the keyboard shortcut."""
704        _keyboard = keyboard.Keyboard()
705        _keyboard.press_key('ctrl+shift+q')
706        _keyboard.press_key('ctrl+shift+q')
707        _keyboard.close()
708