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