xref: /aosp_15_r20/external/autotest/server/cros/debugd_dev_tools.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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    """USB boot configuration tool."""
359
360
361    def is_enabled(self):
362        return self._check_enabled(DEV_FEATURE_BOOT_FROM_USB_ENABLED)
363
364
365    def enable(self):
366        _send_debugd_command(self._host, 'EnableBootFromUsb')
367
368
369    def disable(self):
370        self._host.run('crossystem dev_boot_usb=0')
371
372
373    def save_state(self):
374        self.initial_state = self.is_enabled()
375
376
377    def restore_state(self):
378        if self.initial_state:
379            self.enable()
380        else:
381            self.disable()
382
383
384class SshServerTool(DevTool):
385    """
386    SSH server tool.
387
388    SSH configuration has two components, the init file and the test
389    keys. Since a system could potentially have none, just the init
390    file, or all files, we want to be sure to restore just the files
391    that existed before the test started.
392    """
393
394
395    PATHS = ('/etc/init/openssh-server.conf',
396             '/root/.ssh/authorized_keys',
397             '/root/.ssh/id_rsa',
398             '/root/.ssh/id_rsa.pub')
399
400
401    def is_enabled(self):
402        return self._check_enabled(DEV_FEATURE_SSH_SERVER_CONFIGURED)
403
404
405    def enable(self):
406        _send_debugd_command(self._host, 'ConfigureSshServer')
407
408
409    def disable(self):
410        for path in self.PATHS:
411            self._host.run('rm -f %s' % path)
412
413
414    def save_state(self):
415        self._save_files(self.PATHS)
416
417
418    def restore_state(self):
419        self._restore_files(self.PATHS)
420
421
422class SystemPasswordTool(DevTool):
423    """
424    System password configuration tool.
425
426    This tool just affects the system password (/etc/shadow). We could
427    add a devmode password tool if we want to explicitly test that as
428    well.
429    """
430
431
432    SYSTEM_PATHS = ('/etc/shadow',)
433    DEV_PATHS = ('/mnt/stateful_partition/etc/devmode.passwd',)
434
435
436    def is_enabled(self):
437        return self._check_enabled(DEV_FEATURE_SYSTEM_ROOT_PASSWORD_SET)
438
439
440    def enable(self):
441        # Save the devmode.passwd file to avoid affecting it.
442        self._save_files(self.DEV_PATHS)
443        try:
444            _send_debugd_command(self._host, 'SetUserPassword',
445                                 ('string:root', 'string:test0000'))
446        finally:
447            # Restore devmode.passwd
448            self._restore_files(self.DEV_PATHS)
449
450
451    def disable(self):
452        self._host.run('passwd -d root')
453
454
455    def save_state(self):
456        self._save_files(self.SYSTEM_PATHS)
457
458
459    def restore_state(self):
460        self._restore_files(self.SYSTEM_PATHS)
461