xref: /aosp_15_r20/external/autotest/client/cros/network_chroot.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li# Lint as: python2, python3
2*9c5db199SXin Li# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be
4*9c5db199SXin Li# found in the LICENSE file.
5*9c5db199SXin Li
6*9c5db199SXin Lifrom __future__ import absolute_import
7*9c5db199SXin Lifrom __future__ import division
8*9c5db199SXin Lifrom __future__ import print_function
9*9c5db199SXin Li
10*9c5db199SXin Liimport errno
11*9c5db199SXin Liimport os
12*9c5db199SXin Liimport shutil
13*9c5db199SXin Liimport six
14*9c5db199SXin Liimport time
15*9c5db199SXin Li
16*9c5db199SXin Lifrom autotest_lib.client.bin import utils
17*9c5db199SXin Li
18*9c5db199SXin Liclass NetworkChroot(object):
19*9c5db199SXin Li    """Implements a chroot environment that runs in a separate network
20*9c5db199SXin Li    namespace from the caller.  This is useful for network tests that
21*9c5db199SXin Li    involve creating a server on the other end of a virtual ethernet
22*9c5db199SXin Li    pair.  This object is initialized with an interface name to pass
23*9c5db199SXin Li    to the chroot, as well as the IP address to assign to this
24*9c5db199SXin Li    interface, since in passing the interface into the chroot, any
25*9c5db199SXin Li    pre-configured address is removed.
26*9c5db199SXin Li
27*9c5db199SXin Li    The startup of the chroot is an orchestrated process where a
28*9c5db199SXin Li    small startup script is run to perform the following tasks:
29*9c5db199SXin Li      - Write out pid file which will be a handle to the
30*9c5db199SXin Li        network namespace that that |interface| should be passed to.
31*9c5db199SXin Li      - Wait for the network namespace to be passed in, by performing
32*9c5db199SXin Li        a "sleep" and writing the pid of this process as well.  Our
33*9c5db199SXin Li        parent will kill this process to resume the startup process.
34*9c5db199SXin Li      - We can now configure the network interface with an address.
35*9c5db199SXin Li      - At this point, we can now start any user-requested server
36*9c5db199SXin Li        processes.
37*9c5db199SXin Li    """
38*9c5db199SXin Li    BIND_ROOT_DIRECTORIES = ('bin', 'dev', 'dev/pts', 'lib', 'lib32', 'lib64',
39*9c5db199SXin Li                             'proc', 'sbin', 'sys', 'usr', 'usr/local')
40*9c5db199SXin Li    # Subset of BIND_ROOT_DIRECTORIES that should be mounted writable.
41*9c5db199SXin Li    BIND_ROOT_WRITABLE_DIRECTORIES = frozenset(('dev/pts',))
42*9c5db199SXin Li    # Directories we'll bind mount when we want to bridge DBus namespaces.
43*9c5db199SXin Li    # Includes directories containing the system bus socket and machine ID.
44*9c5db199SXin Li    DBUS_BRIDGE_DIRECTORIES = ('run/dbus/', 'var/lib/dbus/')
45*9c5db199SXin Li
46*9c5db199SXin Li    ROOT_DIRECTORIES = ('etc', 'etc/ssl', 'tmp', 'var', 'var/log', 'run',
47*9c5db199SXin Li                        'run/lock')
48*9c5db199SXin Li    ROOT_SYMLINKS = (
49*9c5db199SXin Li        ('var/run', '/run'),
50*9c5db199SXin Li        ('var/lock', '/run/lock'),
51*9c5db199SXin Li    )
52*9c5db199SXin Li    STARTUP = 'etc/chroot_startup.sh'
53*9c5db199SXin Li    STARTUP_DELAY_SECONDS = 5
54*9c5db199SXin Li    STARTUP_PID_FILE = 'run/vpn_startup.pid'
55*9c5db199SXin Li    STARTUP_SLEEPER_PID_FILE = 'run/vpn_sleeper.pid'
56*9c5db199SXin Li    COPIED_CONFIG_FILES = [
57*9c5db199SXin Li        'etc/ld.so.cache',
58*9c5db199SXin Li        'etc/ssl/openssl.cnf.compat'
59*9c5db199SXin Li    ]
60*9c5db199SXin Li    CONFIG_FILE_TEMPLATES = {
61*9c5db199SXin Li        STARTUP:
62*9c5db199SXin Li            '#!/bin/sh\n'
63*9c5db199SXin Li            'exec > /var/log/startup.log 2>&1\n'
64*9c5db199SXin Li            'set -x\n'
65*9c5db199SXin Li            'echo $$ > /%(startup-pidfile)s\n'
66*9c5db199SXin Li            'sleep %(startup-delay-seconds)d &\n'
67*9c5db199SXin Li            'echo $! > /%(sleeper-pidfile)s &\n'
68*9c5db199SXin Li            'wait\n'
69*9c5db199SXin Li            'ip addr add %(local-ip-and-prefix)s dev %(local-interface-name)s\n'
70*9c5db199SXin Li            'ip link set %(local-interface-name)s up\n'
71*9c5db199SXin Li            # For running strongSwan VPN with flag --with-piddir=/run/ipsec. We
72*9c5db199SXin Li            # want to use /run/ipsec for strongSwan runtime data dir instead of
73*9c5db199SXin Li            # /run, and the cmdline flag applies to both client and server.
74*9c5db199SXin Li            'mkdir -p /run/ipsec\n'
75*9c5db199SXin Li    }
76*9c5db199SXin Li    CONFIG_FILE_VALUES = {
77*9c5db199SXin Li        'sleeper-pidfile': STARTUP_SLEEPER_PID_FILE,
78*9c5db199SXin Li        'startup-delay-seconds': STARTUP_DELAY_SECONDS,
79*9c5db199SXin Li        'startup-pidfile': STARTUP_PID_FILE
80*9c5db199SXin Li    }
81*9c5db199SXin Li
82*9c5db199SXin Li    def __init__(self, interface, address, prefix):
83*9c5db199SXin Li        self._interface = interface
84*9c5db199SXin Li
85*9c5db199SXin Li        # Copy these values from the class-static since specific instances
86*9c5db199SXin Li        # of this class are allowed to modify their contents.
87*9c5db199SXin Li        self._bind_root_directories = list(self.BIND_ROOT_DIRECTORIES)
88*9c5db199SXin Li        self._root_directories = list(self.ROOT_DIRECTORIES)
89*9c5db199SXin Li        self._copied_config_files = list(self.COPIED_CONFIG_FILES)
90*9c5db199SXin Li        self._config_file_templates = self.CONFIG_FILE_TEMPLATES.copy()
91*9c5db199SXin Li        self._config_file_values = self.CONFIG_FILE_VALUES.copy()
92*9c5db199SXin Li        self._env = dict(os.environ)
93*9c5db199SXin Li
94*9c5db199SXin Li        self._config_file_values.update({
95*9c5db199SXin Li            'local-interface-name': interface,
96*9c5db199SXin Li            'local-ip': address,
97*9c5db199SXin Li            'local-ip-and-prefix': '%s/%d' % (address, prefix)
98*9c5db199SXin Li        })
99*9c5db199SXin Li
100*9c5db199SXin Li
101*9c5db199SXin Li    def startup(self):
102*9c5db199SXin Li        """Create the chroot and start user processes."""
103*9c5db199SXin Li        self.make_chroot()
104*9c5db199SXin Li        self.write_configs()
105*9c5db199SXin Li        self.run(['/bin/bash', os.path.join('/', self.STARTUP), '&'])
106*9c5db199SXin Li        self.move_interface_to_chroot_namespace()
107*9c5db199SXin Li        self.kill_pid_file(self.STARTUP_SLEEPER_PID_FILE)
108*9c5db199SXin Li
109*9c5db199SXin Li
110*9c5db199SXin Li    def shutdown(self):
111*9c5db199SXin Li        """Remove the chroot filesystem in which the VPN server was running"""
112*9c5db199SXin Li        # TODO(pstew): Some processes take a while to exit, which will cause
113*9c5db199SXin Li        # the cleanup below to fail to complete successfully...
114*9c5db199SXin Li        time.sleep(10)
115*9c5db199SXin Li        utils.system_output('rm -rf --one-file-system %s' % self._temp_dir,
116*9c5db199SXin Li                            ignore_status=True)
117*9c5db199SXin Li
118*9c5db199SXin Li
119*9c5db199SXin Li    def add_config_templates(self, template_dict):
120*9c5db199SXin Li        """Add a filename-content dict to the set of templates for the chroot
121*9c5db199SXin Li
122*9c5db199SXin Li        @param template_dict dict containing filename-content pairs for
123*9c5db199SXin Li            templates to be applied to the chroot.  The keys to this dict
124*9c5db199SXin Li            should not contain a leading '/'.
125*9c5db199SXin Li
126*9c5db199SXin Li        """
127*9c5db199SXin Li        self._config_file_templates.update(template_dict)
128*9c5db199SXin Li
129*9c5db199SXin Li
130*9c5db199SXin Li    def add_config_values(self, value_dict):
131*9c5db199SXin Li        """Add a name-value dict to the set of values for the config template
132*9c5db199SXin Li
133*9c5db199SXin Li        @param value_dict dict containing key-value pairs of values that will
134*9c5db199SXin Li            be applied to the config file templates.
135*9c5db199SXin Li
136*9c5db199SXin Li        """
137*9c5db199SXin Li        self._config_file_values.update(value_dict)
138*9c5db199SXin Li
139*9c5db199SXin Li
140*9c5db199SXin Li    def add_copied_config_files(self, files):
141*9c5db199SXin Li        """Add |files| to the set to be copied to the chroot.
142*9c5db199SXin Li
143*9c5db199SXin Li        @param files iterable object containing a list of files to
144*9c5db199SXin Li            be copied into the chroot.  These elements should not contain a
145*9c5db199SXin Li            leading '/'.
146*9c5db199SXin Li
147*9c5db199SXin Li        """
148*9c5db199SXin Li        self._copied_config_files += files
149*9c5db199SXin Li
150*9c5db199SXin Li
151*9c5db199SXin Li    def add_root_directories(self, directories):
152*9c5db199SXin Li        """Add |directories| to the set created within the chroot.
153*9c5db199SXin Li
154*9c5db199SXin Li        @param directories list/tuple containing a list of directories to
155*9c5db199SXin Li            be created in the chroot.  These elements should not contain a
156*9c5db199SXin Li            leading '/'.
157*9c5db199SXin Li
158*9c5db199SXin Li        """
159*9c5db199SXin Li        self._root_directories += directories
160*9c5db199SXin Li
161*9c5db199SXin Li
162*9c5db199SXin Li    def add_startup_command(self, command):
163*9c5db199SXin Li        """Add a command to the script run when the chroot starts up.
164*9c5db199SXin Li
165*9c5db199SXin Li        @param command string containing the command line to run.
166*9c5db199SXin Li
167*9c5db199SXin Li        """
168*9c5db199SXin Li        self._config_file_templates[self.STARTUP] += '%s\n' % command
169*9c5db199SXin Li
170*9c5db199SXin Li
171*9c5db199SXin Li    def add_environment(self, env_dict):
172*9c5db199SXin Li        """Add variables to the chroot environment.
173*9c5db199SXin Li
174*9c5db199SXin Li        @param env_dict dict dictionary containing environment variables
175*9c5db199SXin Li        """
176*9c5db199SXin Li        self._env.update(env_dict)
177*9c5db199SXin Li
178*9c5db199SXin Li
179*9c5db199SXin Li    def get_log_contents(self):
180*9c5db199SXin Li        """Return the logfiles from the chroot."""
181*9c5db199SXin Li        return utils.system_output("head -10000 %s" %
182*9c5db199SXin Li                                   self.chroot_path("var/log/*"))
183*9c5db199SXin Li
184*9c5db199SXin Li
185*9c5db199SXin Li    def bridge_dbus_namespaces(self):
186*9c5db199SXin Li        """Make the system DBus daemon visible inside the chroot."""
187*9c5db199SXin Li        # Need the system socket and the machine-id.
188*9c5db199SXin Li        self._bind_root_directories += self.DBUS_BRIDGE_DIRECTORIES
189*9c5db199SXin Li
190*9c5db199SXin Li
191*9c5db199SXin Li    def chroot_path(self, path):
192*9c5db199SXin Li        """Returns the the path within the chroot for |path|.
193*9c5db199SXin Li
194*9c5db199SXin Li        @param path string filename within the choot.  This should not
195*9c5db199SXin Li            contain a leading '/'.
196*9c5db199SXin Li
197*9c5db199SXin Li        """
198*9c5db199SXin Li        return os.path.join(self._temp_dir, path.lstrip('/'))
199*9c5db199SXin Li
200*9c5db199SXin Li
201*9c5db199SXin Li    def get_pid_file(self, pid_file, missing_ok=False):
202*9c5db199SXin Li        """Returns the integer contents of |pid_file| in the chroot.
203*9c5db199SXin Li
204*9c5db199SXin Li        @param pid_file string containing the filename within the choot
205*9c5db199SXin Li            to read and convert to an integer.  This should not contain a
206*9c5db199SXin Li            leading '/'.
207*9c5db199SXin Li        @param missing_ok bool indicating whether exceptions due to failure
208*9c5db199SXin Li            to open the pid file should be caught.  If true a missing pid
209*9c5db199SXin Li            file will cause this method to return 0.  If false, a missing
210*9c5db199SXin Li            pid file will cause an exception.
211*9c5db199SXin Li
212*9c5db199SXin Li        """
213*9c5db199SXin Li        chroot_pid_file = self.chroot_path(pid_file)
214*9c5db199SXin Li        try:
215*9c5db199SXin Li            with open(chroot_pid_file) as f:
216*9c5db199SXin Li                return int(f.read())
217*9c5db199SXin Li        except IOError as e:
218*9c5db199SXin Li            if not missing_ok or e.errno != errno.ENOENT:
219*9c5db199SXin Li                raise e
220*9c5db199SXin Li
221*9c5db199SXin Li            return 0
222*9c5db199SXin Li
223*9c5db199SXin Li
224*9c5db199SXin Li    def kill_pid_file(self, pid_file, missing_ok=False):
225*9c5db199SXin Li        """Kills the process belonging to |pid_file| in the chroot.
226*9c5db199SXin Li
227*9c5db199SXin Li        @param pid_file string filename within the chroot to gain the process ID
228*9c5db199SXin Li            which this method will kill.
229*9c5db199SXin Li        @param missing_ok bool indicating whether a missing pid file is okay,
230*9c5db199SXin Li            and should be ignored.
231*9c5db199SXin Li
232*9c5db199SXin Li        """
233*9c5db199SXin Li        pid = self.get_pid_file(pid_file, missing_ok=missing_ok)
234*9c5db199SXin Li        if missing_ok and pid == 0:
235*9c5db199SXin Li            return
236*9c5db199SXin Li        utils.system('kill %d' % pid, ignore_status=True)
237*9c5db199SXin Li
238*9c5db199SXin Li
239*9c5db199SXin Li    def make_chroot(self):
240*9c5db199SXin Li        """Make a chroot filesystem."""
241*9c5db199SXin Li        self._temp_dir = utils.system_output(
242*9c5db199SXin Li                'mktemp -d /usr/local/tmp/chroot.XXXXXXXXX')
243*9c5db199SXin Li        utils.system('chmod go+rX %s' % self._temp_dir)
244*9c5db199SXin Li        for rootdir in self._root_directories:
245*9c5db199SXin Li            os.mkdir(self.chroot_path(rootdir))
246*9c5db199SXin Li
247*9c5db199SXin Li        self._jail_args = []
248*9c5db199SXin Li        for rootdir in self._bind_root_directories:
249*9c5db199SXin Li            src_path = os.path.join('/', rootdir)
250*9c5db199SXin Li            dst_path = self.chroot_path(rootdir)
251*9c5db199SXin Li            if not os.path.exists(src_path):
252*9c5db199SXin Li                continue
253*9c5db199SXin Li            elif os.path.islink(src_path):
254*9c5db199SXin Li                link_path = os.readlink(src_path)
255*9c5db199SXin Li                os.symlink(link_path, dst_path)
256*9c5db199SXin Li            else:
257*9c5db199SXin Li                os.makedirs(dst_path)  # Recursively create directories.
258*9c5db199SXin Li                mount_arg = '%s,%s' % (src_path, src_path)
259*9c5db199SXin Li                if rootdir in self.BIND_ROOT_WRITABLE_DIRECTORIES:
260*9c5db199SXin Li                    mount_arg += ',1'
261*9c5db199SXin Li                self._jail_args += [ '-b', mount_arg ]
262*9c5db199SXin Li
263*9c5db199SXin Li        for config_file in self._copied_config_files:
264*9c5db199SXin Li            src_path = os.path.join('/', config_file)
265*9c5db199SXin Li            dst_path = self.chroot_path(config_file)
266*9c5db199SXin Li            if os.path.exists(src_path):
267*9c5db199SXin Li                shutil.copyfile(src_path, dst_path)
268*9c5db199SXin Li
269*9c5db199SXin Li        for src_path, target_path in self.ROOT_SYMLINKS:
270*9c5db199SXin Li            link_path = self.chroot_path(src_path)
271*9c5db199SXin Li            os.symlink(target_path, link_path)
272*9c5db199SXin Li
273*9c5db199SXin Li
274*9c5db199SXin Li    def move_interface_to_chroot_namespace(self):
275*9c5db199SXin Li        """Move network interface to the network namespace of the server."""
276*9c5db199SXin Li        utils.system('ip link set %s netns %d' %
277*9c5db199SXin Li                     (self._interface,
278*9c5db199SXin Li                      self.get_pid_file(self.STARTUP_PID_FILE)))
279*9c5db199SXin Li
280*9c5db199SXin Li
281*9c5db199SXin Li    def run(self, args, ignore_status=False):
282*9c5db199SXin Li        """Run a command in a chroot, within a separate network namespace.
283*9c5db199SXin Li
284*9c5db199SXin Li        @param args list containing the command line arguments to run.
285*9c5db199SXin Li        @param ignore_status bool set to true if a failure should be ignored.
286*9c5db199SXin Li
287*9c5db199SXin Li        """
288*9c5db199SXin Li        utils.run('minijail0 -e -C %s %s' %
289*9c5db199SXin Li                  (self._temp_dir, ' '.join(self._jail_args + args)),
290*9c5db199SXin Li                  timeout=None,
291*9c5db199SXin Li                  ignore_status=ignore_status,
292*9c5db199SXin Li                  stdout_tee=utils.TEE_TO_LOGS,
293*9c5db199SXin Li                  stderr_tee=utils.TEE_TO_LOGS,
294*9c5db199SXin Li                  env=self._env)
295*9c5db199SXin Li
296*9c5db199SXin Li
297*9c5db199SXin Li    def write_configs(self):
298*9c5db199SXin Li        """Write out config files"""
299*9c5db199SXin Li        for config_file, template in six.iteritems(self._config_file_templates):
300*9c5db199SXin Li            with open(self.chroot_path(config_file), 'w') as f:
301*9c5db199SXin Li                f.write(template % self._config_file_values)
302