• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2014 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import logging
6import os
7
8import common
9from autotest_lib.client.common_lib import error
10
11"""
12Functions to query and control debugd dev tools.
13
14This file provides a set of functions to check the general state of the
15debugd dev tools, and a set of classes to interface to the individual
16tools.
17
18Current tool classes are:
19    RootfsVerificationTool
20    BootFromUsbTool
21    SshServerTool
22    SystemPasswordTool
23These classes have functions to check the state and enable/disable the
24tool. Some tools may not be able to disable themselves, in which case
25an exception will be thrown (for example, RootfsVerificationTool cannot
26be disabled).
27
28General usage will look something like this:
29
30# Make sure tools are accessible on the system.
31if debugd_dev_tools.are_dev_tools_available(host):
32    # Create the tool(s) you want to interact with.
33    tools = [debugd_dev_tools.SshServerTool(), ...]
34    for tool in tools:
35        # Initialize tools and save current state.
36        tool.initialize(host, save_initial_state=True)
37        # Perform required action with tools.
38        tool.enable()
39        # Restore initial tool state.
40        tool.restore_state()
41    # Clean up temporary files.
42    debugd_dev_tools.remove_temp_files()
43"""
44
45
46# Defined in system_api/dbus/service_constants.h.
47DEV_FEATURES_DISABLED = 1 << 0
48DEV_FEATURE_ROOTFS_VERIFICATION_REMOVED = 1 << 1
49DEV_FEATURE_BOOT_FROM_USB_ENABLED = 1 << 2
50DEV_FEATURE_SSH_SERVER_CONFIGURED = 1 << 3
51DEV_FEATURE_DEV_MODE_ROOT_PASSWORD_SET = 1 << 4
52DEV_FEATURE_SYSTEM_ROOT_PASSWORD_SET = 1 << 5
53
54
55# Location to save temporary files to store and load state. This folder should
56# be persistent through a power cycle so we can't use /tmp.
57_TEMP_DIR = '/usr/local/autotest/tmp/debugd_dev_tools'
58
59
60class AccessError(error.CmdError):
61    """Raised when debugd D-Bus access fails."""
62    pass
63
64
65class FeatureUnavailableError(error.TestNAError):
66    """Raised when a feature cannot be enabled or disabled."""
67    pass
68
69
70def query_dev_tools_state(host):
71    """
72    Queries debugd for the current dev features state.
73
74    @param host: Host device.
75
76    @return: Integer debugd query return value.
77
78    @raise AccessError: Can't talk to debugd on the host.
79    """
80    result = _send_debugd_command(host, 'QueryDevFeatures')
81    state = int(result.stdout)
82    logging.debug('query_dev_tools_state = %d (0x%04X)', state, state)
83    return state
84
85
86def are_dev_tools_available(host):
87    """
88    Check if dev tools are available on the host.
89
90    @param host: Host device.
91
92    @return: True if tools are available, False otherwise.
93    """
94    try:
95        return query_dev_tools_state(host) != DEV_FEATURES_DISABLED
96    except AccessError:
97        return False
98
99
100def remove_temp_files(host):
101    """
102    Removes all DevTools temporary files and directories.
103
104    Any test using dev tools should try to call this just before
105    exiting to erase any temporary files that may have been saved.
106
107    @param host: Host device.
108    """
109    host.run('rm -rf "%s"' % _TEMP_DIR)
110
111
112def expect_access_failure(host, tools):
113    """
114    Verifies that access is denied to all provided tools.
115
116    Will check are_dev_tools_available() first to try to avoid changing
117    device state in case access is allowed. Otherwise, the function
118    will try to enable each tool in the list and throw an exception if
119    any succeeds.
120
121    @param host: Host device.
122    @param tools: List of tools to checks.
123
124    @raise TestFail: are_dev_tools_available() returned True or
125                     a tool successfully enabled.
126    """
127    if are_dev_tools_available(host):
128        raise error.TestFail('Unexpected dev tool access success')
129    for tool in tools:
130        try:
131            tool.enable()
132        except AccessError:
133            # We want an exception, otherwise the tool succeeded.
134            pass
135        else:
136            raise error.TestFail('Unexpected %s enable success.' % tool)
137
138
139def _send_debugd_command(host, name, args=()):
140    """
141    Sends a debugd command.
142
143    @param host: Host to run the command on.
144    @param name: String debugd D-Bus function name.
145    @param args: List of string arguments to pass to dbus-send.
146
147    @return: The dbus-send CmdResult object.
148
149    @raise AccessError: debugd call returned an error.
150    """
151    command = ('dbus-send --system --fixed --print-reply '
152               '--dest=org.chromium.debugd /org/chromium/debugd '
153               '"org.chromium.debugd.%s"' % name)
154    for arg in args:
155        command += ' %s' % arg
156    try:
157        return host.run(command)
158    except error.CmdError as e:
159        raise AccessError(e.command, e.result_obj, e.additional_text)
160
161
162class DevTool(object):
163    """
164    Parent tool class.
165
166    Each dev tool has its own child class that handles the details
167    of disabling, enabling, and querying the functionality. This class
168    provides some common functionality needed by multiple tools.
169
170    Child classes should implement the following:
171      - is_enabled(): use debugd to query whether the tool is enabled.
172      - enable(): use debugd to enable the tool.
173      - disable(): manually disable the tool.
174      - save_state(): record the current tool state on the host.
175      - restore_state(): restore the saved tool state.
176
177    If a child class cannot perform the required action (for
178    example the rootfs tool can't currently restore its initial
179    state), leave the function unimplemented so it will throw an
180    exception if a test attempts to use it.
181    """
182
183
184    def initialize(self, host, save_initial_state=False):
185        """
186        Sets up the initial tool state. This must be called on
187        every tool before use.
188
189        @param host: Device host the test is running on.
190        @param save_initial_state: True to save the device state.
191        """
192        self._host = host
193        if save_initial_state:
194            self.save_state()
195
196
197    def is_enabled(self):
198        """
199        Each tool should override this to query itself using debugd.
200        Normally this can be done by using the provided
201        _check_enabled() function.
202        """
203        self._unimplemented_function_error('is_enabled')
204
205
206    def enable(self):
207        """
208        Each tool should override this to enable itself using debugd.
209        """
210        self._unimplemented_function_error('enable')
211
212
213    def disable(self):
214        """
215        Each tool should override this to disable itself.
216        """
217        self._unimplemented_function_error('disable')
218
219
220    def save_state(self):
221        """
222        Save the initial tool state. Should be overridden by child
223        tool classes.
224        """
225        self._unimplemented_function_error('_save_state')
226
227
228    def restore_state(self):
229        """
230        Restore the initial tool state. Should be overridden by child
231        tool classes.
232        """
233        self._unimplemented_function_error('_restore_state')
234
235
236    def _check_enabled(self, bits):
237        """
238        Checks if the given feature is currently enabled according to
239        the debugd status query function.
240
241        @param bits: Integer status bits corresponding to the features.
242
243        @return: True if the status query is enabled and the
244                 indicated bits are all set, False otherwise.
245        """
246        state = query_dev_tools_state(self._host)
247        enabled = bool((state != DEV_FEATURES_DISABLED) and
248                       (state & bits == bits))
249        logging.debug('%s _check_enabled = %s (0x%04X / 0x%04X)',
250                      self, enabled, state, bits)
251        return enabled
252
253
254    def _get_temp_path(self, source_path):
255        """
256        Get temporary storage path for a file or directory.
257
258        Temporary path is based on the tool class name and the
259        source directory to keep tool files isolated and prevent
260        name conflicts within tools.
261
262        The function returns a full temporary path corresponding to
263        |source_path|.
264
265        For example, _get_temp_path('/foo/bar.txt') would return
266        '/path/to/temp/folder/debugd_dev_tools/FooTool/foo/bar.txt'.
267
268        @param source_path: String path to the file or directory.
269
270        @return: Temp path string.
271        """
272        return '%s/%s/%s' % (_TEMP_DIR, self, source_path)
273
274
275    def _save_files(self, paths):
276        """
277        Saves a set of files to a temporary location.
278
279        This can be used to save specific files so that a tool can
280        save its current state before starting a test.
281
282        See _restore_files() for restoring the saved files.
283
284        @param paths: List of string paths to save.
285        """
286        for path in paths:
287            temp_path = self._get_temp_path(path)
288            self._host.run('mkdir -p "%s"' % os.path.dirname(temp_path))
289            self._host.run('cp -r "%s" "%s"' % (path, temp_path),
290                           ignore_status=True)
291
292
293    def _restore_files(self, paths):
294        """
295        Restores saved files to their original location.
296
297        Used to restore files that have previously been saved by
298        _save_files(), usually to return the device to its initial
299        state.
300
301        This function does not erase the saved files, so it can
302        be used multiple times if needed.
303
304        @param paths: List of string paths to restore.
305        """
306        for path in paths:
307            self._host.run('rm -rf "%s"' % path)
308            self._host.run('cp -r "%s" "%s"' % (self._get_temp_path(path),
309                                                path),
310                           ignore_status=True)
311
312
313    def _unimplemented_function_error(self, function_name):
314        """
315        Throws an exception if a required tool function hasn't been
316        implemented.
317        """
318        raise FeatureUnavailableError('%s has not implemented %s()' %
319                                      (self, function_name))
320
321
322    def __str__(self):
323        """
324        Tool name accessor for temporary files and logging.
325
326        Based on class rather than unique instance naming since all
327        instances of the same tool have identical functionality.
328        """
329        return type(self).__name__
330
331
332class RootfsVerificationTool(DevTool):
333    """
334    Rootfs verification removal tool.
335
336    This tool is currently unable to transition from non-verified back
337    to verified rootfs; it may potentially require re-flashing an OS.
338    Since devices in the test lab run in verified mode, this tool is
339    unsuitable for automated testing until this capability is
340    implemented.
341    """
342
343
344    def is_enabled(self):
345        return self._check_enabled(DEV_FEATURE_ROOTFS_VERIFICATION_REMOVED)
346
347
348    def enable(self):
349        _send_debugd_command(self._host, 'RemoveRootfsVerification')
350        self._host.reboot()
351
352
353    def disable(self):
354        raise FeatureUnavailableError('Cannot re-enable rootfs verification')
355
356
357class BootFromUsbTool(DevTool):
358    """
359    USB boot configuration tool.
360
361    Certain boards have restrictions with USB booting. Mario can't
362    boot from USB at all, and Alex/ZGB can't disable USB booting
363    once it's been enabled. Any attempts to perform these operation
364    will raise a FeatureUnavailableError exception.
365    """
366
367
368    # Lists of which platforms can't enable or disable USB booting.
369    ENABLE_UNAVAILABLE_PLATFORMS = ('mario',)
370    DISABLE_UNAVAILABLE_PLATFORMS = ('mario', 'alex', 'zgb')
371
372
373    def is_enabled(self):
374        return self._check_enabled(DEV_FEATURE_BOOT_FROM_USB_ENABLED)
375
376
377    def enable(self):
378        platform = self._host.get_platform().lower()
379        if any(p in platform for p in self.ENABLE_UNAVAILABLE_PLATFORMS):
380            raise FeatureUnavailableError('USB boot unavilable on %s' %
381                                          platform)
382        _send_debugd_command(self._host, 'EnableBootFromUsb')
383
384
385    def disable(self):
386        platform = self._host.get_platform().lower()
387        if any(p in platform for p in self.DISABLE_UNAVAILABLE_PLATFORMS):
388            raise FeatureUnavailableError("Can't disable USB boot on %s" %
389                                          platform)
390        self._host.run('crossystem dev_boot_usb=0')
391
392
393    def save_state(self):
394        self.initial_state = self.is_enabled()
395
396
397    def restore_state(self):
398        if self.initial_state:
399            self.enable()
400        else:
401            self.disable()
402
403
404class SshServerTool(DevTool):
405    """
406    SSH server tool.
407
408    SSH configuration has two components, the init file and the test
409    keys. Since a system could potentially have none, just the init
410    file, or all files, we want to be sure to restore just the files
411    that existed before the test started.
412    """
413
414
415    PATHS = ('/etc/init/openssh-server.conf',
416             '/root/.ssh/authorized_keys',
417             '/root/.ssh/id_rsa',
418             '/root/.ssh/id_rsa.pub')
419
420
421    def is_enabled(self):
422        return self._check_enabled(DEV_FEATURE_SSH_SERVER_CONFIGURED)
423
424
425    def enable(self):
426        _send_debugd_command(self._host, 'ConfigureSshServer')
427
428
429    def disable(self):
430        for path in self.PATHS:
431            self._host.run('rm -f %s' % path)
432
433
434    def save_state(self):
435        self._save_files(self.PATHS)
436
437
438    def restore_state(self):
439        self._restore_files(self.PATHS)
440
441
442class SystemPasswordTool(DevTool):
443    """
444    System password configuration tool.
445
446    This tool just affects the system password (/etc/shadow). We could
447    add a devmode password tool if we want to explicitly test that as
448    well.
449    """
450
451
452    SYSTEM_PATHS = ('/etc/shadow',)
453    DEV_PATHS = ('/mnt/stateful_partition/etc/devmode.passwd',)
454
455
456    def is_enabled(self):
457        return self._check_enabled(DEV_FEATURE_SYSTEM_ROOT_PASSWORD_SET)
458
459
460    def enable(self):
461        # Save the devmode.passwd file to avoid affecting it.
462        self._save_files(self.DEV_PATHS)
463        try:
464            _send_debugd_command(self._host, 'SetUserPassword',
465                                 ('string:root', 'string:test0000'))
466        finally:
467            # Restore devmode.passwd
468            self._restore_files(self.DEV_PATHS)
469
470
471    def disable(self):
472        self._host.run('passwd -d root')
473
474
475    def save_state(self):
476        self._save_files(self.SYSTEM_PATHS)
477
478
479    def restore_state(self):
480        self._restore_files(self.SYSTEM_PATHS)
481