xref: /aosp_15_r20/external/autotest/server/cros/chrome_sideloader.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li# Copyright 2021 The Chromium OS Authors. All rights reserved.
2*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be
3*9c5db199SXin Li# found in the LICENSE file.
4*9c5db199SXin Li
5*9c5db199SXin Liimport base64
6*9c5db199SXin Liimport logging
7*9c5db199SXin Liimport os
8*9c5db199SXin Liimport json
9*9c5db199SXin Liimport random
10*9c5db199SXin Liimport stat
11*9c5db199SXin Liimport string
12*9c5db199SXin Li
13*9c5db199SXin Li# Shell command to force unmount a mount point if it is mounted
14*9c5db199SXin LiFORCED_UMOUNT_DIR_IF_MOUNTPOINT_CMD = (
15*9c5db199SXin Li        'if mountpoint -q %(dir)s; then umount -l %(dir)s; fi')
16*9c5db199SXin Li# Shell command to set exec and suid flags
17*9c5db199SXin LiSET_MOUNT_FLAGS_CMD = 'mount -o remount,exec,suid %s'
18*9c5db199SXin Li# Shell command to send SIGHUP to dbus daemon
19*9c5db199SXin LiDBUS_RELOAD_COMMAND = 'killall -HUP dbus-daemon'
20*9c5db199SXin Li
21*9c5db199SXin Li
22*9c5db199SXin Lidef extract_from_image(host, image_name, dest_dir):
23*9c5db199SXin Li    """
24*9c5db199SXin Li    Extract contents of an image to a directory.
25*9c5db199SXin Li
26*9c5db199SXin Li    @param host: The DUT to execute the command on
27*9c5db199SXin Li    @param image_name: Name of image
28*9c5db199SXin Li    @param dest_dir: directory where contents of image will be placed.
29*9c5db199SXin Li
30*9c5db199SXin Li    """
31*9c5db199SXin Li
32*9c5db199SXin Li    if not host.path_exists('/var/lib/imageloader/%s' % image_name):
33*9c5db199SXin Li        raise Exception('Image %s not found on host %s' % (image_name, host))
34*9c5db199SXin Li
35*9c5db199SXin Li    def gen_random_str(length):
36*9c5db199SXin Li        """
37*9c5db199SXin Li        Generate random string
38*9c5db199SXin Li
39*9c5db199SXin Li        @param length: Length of the string
40*9c5db199SXin Li
41*9c5db199SXin Li        @return random string of specified length
42*9c5db199SXin Li
43*9c5db199SXin Li        """
44*9c5db199SXin Li        return ''.join(
45*9c5db199SXin Li                [random.choice(string.hexdigits) for _ in range(length)])
46*9c5db199SXin Li
47*9c5db199SXin Li    image_mount_point = '/tmp/image_%s' % gen_random_str(8)
48*9c5db199SXin Li
49*9c5db199SXin Li    # Create directories from scratch
50*9c5db199SXin Li    host.run(['rm', '-rf', dest_dir])
51*9c5db199SXin Li    host.run(['mkdir', '-p', '--mode', '0755', dest_dir, image_mount_point])
52*9c5db199SXin Li
53*9c5db199SXin Li    try:
54*9c5db199SXin Li        # Mount image and copy content to the destination directory
55*9c5db199SXin Li        host.run([
56*9c5db199SXin Li                'imageloader', '--mount',
57*9c5db199SXin Li                '--mount_component=%s' % image_name,
58*9c5db199SXin Li                '--mount_point=%s' % image_mount_point
59*9c5db199SXin Li        ])
60*9c5db199SXin Li
61*9c5db199SXin Li        host.run(['cp', '-r', '%s/*' % image_mount_point, '%s/' % dest_dir])
62*9c5db199SXin Li    except Exception as e:
63*9c5db199SXin Li        raise Exception(
64*9c5db199SXin Li                'Error extracting content from image %s on host %s ' %
65*9c5db199SXin Li                (image_name, host), e)
66*9c5db199SXin Li    finally:
67*9c5db199SXin Li        # Unmount image and remove the temporary directory
68*9c5db199SXin Li        host.run([
69*9c5db199SXin Li                'imageloader', '--unmount',
70*9c5db199SXin Li                '--mount_point=%s' % image_mount_point
71*9c5db199SXin Li        ])
72*9c5db199SXin Li        host.run(['rm', '-rf', image_mount_point])
73*9c5db199SXin Li
74*9c5db199SXin Li
75*9c5db199SXin Lidef _stop_chrome_if_necessary(host):
76*9c5db199SXin Li    """
77*9c5db199SXin Li    Stop chrome if it is running.
78*9c5db199SXin Li
79*9c5db199SXin Li    @param host: The DUT to execute the command on
80*9c5db199SXin Li
81*9c5db199SXin Li    @return True if chrome was stopped. False otherwise.
82*9c5db199SXin Li
83*9c5db199SXin Li    """
84*9c5db199SXin Li    status = host.run_output('status ui')
85*9c5db199SXin Li    if 'start' in status:
86*9c5db199SXin Li        return host.run('stop ui', ignore_status=True).exit_status == 0
87*9c5db199SXin Li
88*9c5db199SXin Li    return False
89*9c5db199SXin Li
90*9c5db199SXin Li
91*9c5db199SXin Lidef _mount_chrome(host, chrome_dir, chrome_mount_point):
92*9c5db199SXin Li    """
93*9c5db199SXin Li    Mount chrome to a mount point
94*9c5db199SXin Li
95*9c5db199SXin Li    @param host: The DUT to execute the command on
96*9c5db199SXin Li    @param chrome_dir: directory where the chrome binary and artifacts
97*9c5db199SXin Li                       will be placed.
98*9c5db199SXin Li    @param chrome_mount_point: Chrome mount point
99*9c5db199SXin Li
100*9c5db199SXin Li    """
101*9c5db199SXin Li    chrome_stopped = _stop_chrome_if_necessary(host)
102*9c5db199SXin Li    _umount_chrome(host, chrome_mount_point)
103*9c5db199SXin Li
104*9c5db199SXin Li    # Mount chrome to the desired chrome directory
105*9c5db199SXin Li    # Upon restart, this version of chrome will be used instead.
106*9c5db199SXin Li    host.run(['mount', '--rbind', chrome_dir, chrome_mount_point])
107*9c5db199SXin Li
108*9c5db199SXin Li    # Chrome needs partition to have exec and suid flags set
109*9c5db199SXin Li    host.run(SET_MOUNT_FLAGS_CMD % chrome_mount_point)
110*9c5db199SXin Li
111*9c5db199SXin Li    # Send SIGHUP to dbus-daemon to tell it to reload its configs. This won't
112*9c5db199SXin Li    # pick up major changes (bus type, logging, etc.), but all we care about is
113*9c5db199SXin Li    # getting the latest policy from /opt/google/chrome/dbus so that Chrome will
114*9c5db199SXin Li    # be authorized to take ownership of its service names.
115*9c5db199SXin Li    host.run(DBUS_RELOAD_COMMAND, ignore_status=True)
116*9c5db199SXin Li
117*9c5db199SXin Li    if chrome_stopped:
118*9c5db199SXin Li        host.run('start ui', ignore_status=True)
119*9c5db199SXin Li
120*9c5db199SXin Li
121*9c5db199SXin Lidef _umount_chrome(host, chrome_mount_point):
122*9c5db199SXin Li    """
123*9c5db199SXin Li    Unmount chrome
124*9c5db199SXin Li
125*9c5db199SXin Li    @param host: The DUT to execute the command on
126*9c5db199SXin Li    @param chrome_mount_point: Chrome mount point
127*9c5db199SXin Li
128*9c5db199SXin Li    """
129*9c5db199SXin Li    chrome_stopped = _stop_chrome_if_necessary(host)
130*9c5db199SXin Li    # Unmount chrome. Upon restart, the default version of chrome
131*9c5db199SXin Li    # under the root partition will be used.
132*9c5db199SXin Li    try:
133*9c5db199SXin Li        host.run(FORCED_UMOUNT_DIR_IF_MOUNTPOINT_CMD %
134*9c5db199SXin Li                 {'dir': chrome_mount_point})
135*9c5db199SXin Li    except Exception as e:
136*9c5db199SXin Li        raise Exception('Exception during cleanup on host %s' % host, e)
137*9c5db199SXin Li
138*9c5db199SXin Li    if chrome_stopped:
139*9c5db199SXin Li        host.run('start ui', ignore_status=True)
140*9c5db199SXin Li
141*9c5db199SXin Li
142*9c5db199SXin Lidef setup_host(host, chrome_dir, chrome_mount_point):
143*9c5db199SXin Li    """
144*9c5db199SXin Li    Perform setup on host.
145*9c5db199SXin Li
146*9c5db199SXin Li    Mount chrome to point to the version provisioned by TLS.
147*9c5db199SXin Li    The provisioning mechanism of chrome from the chrome builder is
148*9c5db199SXin Li    based on Lacros Tast Test on Skylab (go/lacros-tast-on-skylab).
149*9c5db199SXin Li
150*9c5db199SXin Li    The lacros image provisioned by TLS contains the chrome binary
151*9c5db199SXin Li    and artifacts.
152*9c5db199SXin Li
153*9c5db199SXin Li    @param host: The DUT to execute the command on
154*9c5db199SXin Li    @param chrome_dir: directory where the chrome binary and artifacts
155*9c5db199SXin Li                       will be placed.
156*9c5db199SXin Li    @param chrome_mount_point: Chrome mount point
157*9c5db199SXin Li
158*9c5db199SXin Li    """
159*9c5db199SXin Li    logging.info("Setting up host:%s", host)
160*9c5db199SXin Li    try:
161*9c5db199SXin Li        extract_from_image(host, 'lacros', chrome_dir)
162*9c5db199SXin Li        if chrome_mount_point:
163*9c5db199SXin Li            _mount_chrome(host, '%s/out/Release' % chrome_dir,
164*9c5db199SXin Li                          chrome_mount_point)
165*9c5db199SXin Li    except Exception as e:
166*9c5db199SXin Li        raise Exception(
167*9c5db199SXin Li                'Exception while mounting %s on host %s' %
168*9c5db199SXin Li                (chrome_mount_point, host), e)
169*9c5db199SXin Li
170*9c5db199SXin Li
171*9c5db199SXin Lidef cleanup_host(host, chrome_dir, chrome_mount_point):
172*9c5db199SXin Li    """
173*9c5db199SXin Li    Umount chrome and perform cleanup.
174*9c5db199SXin Li
175*9c5db199SXin Li    @param host: The DUT to execute the command on
176*9c5db199SXin Li    @param chrome_dir: directory where the chrome binary and artifacts
177*9c5db199SXin Li                       is placed.
178*9c5db199SXin Li    @param chrome_mount_point: Chrome mount point
179*9c5db199SXin Li
180*9c5db199SXin Li    """
181*9c5db199SXin Li    logging.info("Unmounting chrome on host: %s", host)
182*9c5db199SXin Li    try:
183*9c5db199SXin Li        if chrome_mount_point:
184*9c5db199SXin Li            _umount_chrome(host, chrome_mount_point)
185*9c5db199SXin Li        host.run(['rm', '-rf', chrome_dir])
186*9c5db199SXin Li    except Exception as e:
187*9c5db199SXin Li        raise Exception('Exception during cleanup on host %s' % host, e)
188*9c5db199SXin Li
189*9c5db199SXin Li
190*9c5db199SXin Lidef get_tast_expr_from_file(host, args_dict, results_dir, base_path=None):
191*9c5db199SXin Li    """
192*9c5db199SXin Li    Get Tast expression from argument dictionary using a file.
193*9c5db199SXin Li    If the tast_expr_file and tast_expr_key are in the dictionary returns the
194*9c5db199SXin Li    tast expression from the file. If either/both args are not in the dict,
195*9c5db199SXin Li    None is returned.
196*9c5db199SXin Li    tast_expr_file expects a file containing a json dictionary which it will
197*9c5db199SXin Li    then use tast_expr_key to pull the tast_expr.
198*9c5db199SXin Li
199*9c5db199SXin Li    The tast_expr_file is a json file containing a dictionary of names to tast
200*9c5db199SXin Li    expressions like:
201*9c5db199SXin Li
202*9c5db199SXin Li    {
203*9c5db199SXin Li    "default": "(\"group:mainline\" && \"dep:lacros\" && !informational)",
204*9c5db199SXin Li    "tast_disabled_tests_from_lacros_example": "(\"group:mainline\" && \"dep:lacros\" && !informational && !\"name:lacros.Basic\")"
205*9c5db199SXin Li    }
206*9c5db199SXin Li
207*9c5db199SXin Li    @param host: Host having the provisioned lacros image with the file
208*9c5db199SXin Li    @param args_dict: Argument dictionary
209*9c5db199SXin Li    @param results_dir: Where to store the tast_expr_file from the dut
210*9c5db199SXin Li    @param base_path: Base path of the provisioned folder
211*9c5db199SXin Li
212*9c5db199SXin Li    """
213*9c5db199SXin Li    tast_expr_file_name = args_dict.get('tast_expr_file')
214*9c5db199SXin Li    tast_expr_key = args_dict.get('tast_expr_key')
215*9c5db199SXin Li    if tast_expr_file_name and tast_expr_key:
216*9c5db199SXin Li        if base_path:
217*9c5db199SXin Li            tast_expr_file_name = os.path.join(base_path, tast_expr_file_name)
218*9c5db199SXin Li
219*9c5db199SXin Li        # Get the tast expr file from the provisioned lacros folder
220*9c5db199SXin Li        if not host.path_exists(tast_expr_file_name):
221*9c5db199SXin Li            raise Exception(
222*9c5db199SXin Li                    'tast_expr_file: %s could not be found on the dut' %
223*9c5db199SXin Li                    tast_expr_file_name)
224*9c5db199SXin Li        local_file_name = os.path.join(results_dir,
225*9c5db199SXin Li                                       os.path.basename(tast_expr_file_name))
226*9c5db199SXin Li        st = os.stat(results_dir)
227*9c5db199SXin Li        os.chmod(results_dir, st.st_mode | stat.S_IWRITE)
228*9c5db199SXin Li        host.get_file(tast_expr_file_name, local_file_name, delete_dest=True)
229*9c5db199SXin Li
230*9c5db199SXin Li        with open(local_file_name) as tast_expr_file:
231*9c5db199SXin Li            expr_dict = json.load(tast_expr_file)
232*9c5db199SXin Li            expr = expr_dict.get(tast_expr_key)
233*9c5db199SXin Li            # If both args were provided, the entry is expected in the file
234*9c5db199SXin Li            if not expr:
235*9c5db199SXin Li                raise Exception('tast_expr_key: %s could not be found' %
236*9c5db199SXin Li                                tast_expr_key)
237*9c5db199SXin Li            logging.info("tast_expr retreived from:%s", tast_expr_file)
238*9c5db199SXin Li            return expr
239*9c5db199SXin Li    return None
240*9c5db199SXin Li
241*9c5db199SXin Li
242*9c5db199SXin Lidef get_tast_expr(args_dict):
243*9c5db199SXin Li    """
244*9c5db199SXin Li    Get Tast expression from argument dictionary.
245*9c5db199SXin Li    Users have options of using tast_expr or tast_expr_b64 in dictionary.
246*9c5db199SXin Li    tast_expr_b64 expects a base64 encoded tast_expr, for instance:
247*9c5db199SXin Li      tast_expr = '("group:mainline" && "dep:lacros")'
248*9c5db199SXin Li      tast_expr_b64 = base64.b64encode(s.encode('utf-8')).decode('ascii')
249*9c5db199SXin Li
250*9c5db199SXin Li    @param args_dict: Argument dictionary
251*9c5db199SXin Li
252*9c5db199SXin Li    """
253*9c5db199SXin Li    expr = args_dict.get('tast_expr')
254*9c5db199SXin Li    if expr:
255*9c5db199SXin Li        return expr
256*9c5db199SXin Li
257*9c5db199SXin Li    expr_b64 = args_dict.get('tast_expr_b64')
258*9c5db199SXin Li    if expr_b64:
259*9c5db199SXin Li        try:
260*9c5db199SXin Li            expr = base64.b64decode(expr_b64).decode()
261*9c5db199SXin Li            return expr
262*9c5db199SXin Li        except Exception as e:
263*9c5db199SXin Li            raise Exception('Failed to decode tast_expr_b64: %s' %
264*9c5db199SXin Li                            expr_b64) from e
265*9c5db199SXin Li
266*9c5db199SXin Li    raise Exception(
267*9c5db199SXin Li            '''Tast expression is unspecified: set tast_expr or tast_expr_b64 in --args.\n'''
268*9c5db199SXin Li            '''  Example: test_that --args="tast_expr=lacros.Basic"\n'''
269*9c5db199SXin Li            '''  If the expression contains spaces, consider transforming it to\n'''
270*9c5db199SXin Li            '''  base64 and passing it via tast_expr_b64 flag.\n'''
271*9c5db199SXin Li            '''  Example:\n'''
272*9c5db199SXin Li            '''    In Python:\n'''
273*9c5db199SXin Li            '''      tast_expr = '("group:mainline" && "dep:lacros")'\n'''
274*9c5db199SXin Li            '''      # Yields 'KCJncm91cDptYWlubGluZSIgJiYgImRlcDpsYWNyb3MiKQ=='\n'''
275*9c5db199SXin Li            '''      tast_expr_b64 = base64.b64encode(s.encode('utf-8')).decode('ascii')\n'''
276*9c5db199SXin Li            '''    Then in Autotest CLI:\n'''
277*9c5db199SXin Li            '''      test_that --args="tast_expr_b64=KCJncm91cDptYWlubGluZSIgJiYgImRlcDpsYWNyb3MiKQ=="\n'''
278*9c5db199SXin Li            '''  More details at go/lacros-on-skylab.''')
279