• 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', 'etc/ssl', 'tmp', 'var', 'var/log', 'run',
41                        'run/lock')
42    ROOT_SYMLINKS = (
43        ('var/run', '/run'),
44        ('var/lock', '/run/lock'),
45    )
46    STARTUP = 'etc/chroot_startup.sh'
47    STARTUP_DELAY_SECONDS = 5
48    STARTUP_PID_FILE = 'run/vpn_startup.pid'
49    STARTUP_SLEEPER_PID_FILE = 'run/vpn_sleeper.pid'
50    COPIED_CONFIG_FILES = [
51        'etc/ld.so.cache',
52        'etc/ssl/openssl.cnf.compat'
53    ]
54    CONFIG_FILE_TEMPLATES = {
55        STARTUP:
56            '#!/bin/sh\n'
57            'exec > /var/log/startup.log 2>&1\n'
58            'set -x\n'
59            'echo $$ > /%(startup-pidfile)s\n'
60            'sleep %(startup-delay-seconds)d &\n'
61            'echo $! > /%(sleeper-pidfile)s &\n'
62            'wait\n'
63            'ip addr add %(local-ip-and-prefix)s dev %(local-interface-name)s\n'
64            'ip link set %(local-interface-name)s up\n'
65            # For running strongSwan VPN with flag --with-piddir=/run/ipsec. We
66            # want to use /run/ipsec for strongSwan runtime data dir instead of
67            # /run, and the cmdline flag applies to both client and server.
68            'mkdir -p /run/ipsec\n'
69    }
70    CONFIG_FILE_VALUES = {
71        'sleeper-pidfile': STARTUP_SLEEPER_PID_FILE,
72        'startup-delay-seconds': STARTUP_DELAY_SECONDS,
73        'startup-pidfile': STARTUP_PID_FILE
74    }
75
76    def __init__(self, interface, address, prefix):
77        self._interface = interface
78
79        # Copy these values from the class-static since specific instances
80        # of this class are allowed to modify their contents.
81        self._bind_root_directories = list(self.BIND_ROOT_DIRECTORIES)
82        self._root_directories = list(self.ROOT_DIRECTORIES)
83        self._copied_config_files = list(self.COPIED_CONFIG_FILES)
84        self._config_file_templates = self.CONFIG_FILE_TEMPLATES.copy()
85        self._config_file_values = self.CONFIG_FILE_VALUES.copy()
86        self._env = dict(os.environ)
87
88        self._config_file_values.update({
89            'local-interface-name': interface,
90            'local-ip': address,
91            'local-ip-and-prefix': '%s/%d' % (address, prefix)
92        })
93
94
95    def startup(self):
96        """Create the chroot and start user processes."""
97        self.make_chroot()
98        self.write_configs()
99        self.run(['/bin/bash', os.path.join('/', self.STARTUP), '&'])
100        self.move_interface_to_chroot_namespace()
101        self.kill_pid_file(self.STARTUP_SLEEPER_PID_FILE)
102
103
104    def shutdown(self):
105        """Remove the chroot filesystem in which the VPN server was running"""
106        # TODO(pstew): Some processes take a while to exit, which will cause
107        # the cleanup below to fail to complete successfully...
108        time.sleep(10)
109        utils.system_output('rm -rf --one-file-system %s' % self._temp_dir,
110                            ignore_status=True)
111
112
113    def add_config_templates(self, template_dict):
114        """Add a filename-content dict to the set of templates for the chroot
115
116        @param template_dict dict containing filename-content pairs for
117            templates to be applied to the chroot.  The keys to this dict
118            should not contain a leading '/'.
119
120        """
121        self._config_file_templates.update(template_dict)
122
123
124    def add_config_values(self, value_dict):
125        """Add a name-value dict to the set of values for the config template
126
127        @param value_dict dict containing key-value pairs of values that will
128            be applied to the config file templates.
129
130        """
131        self._config_file_values.update(value_dict)
132
133
134    def add_copied_config_files(self, files):
135        """Add |files| to the set to be copied to the chroot.
136
137        @param files iterable object containing a list of files to
138            be copied into the chroot.  These elements should not contain a
139            leading '/'.
140
141        """
142        self._copied_config_files += files
143
144
145    def add_root_directories(self, directories):
146        """Add |directories| to the set created within the chroot.
147
148        @param directories list/tuple containing a list of directories to
149            be created in the chroot.  These elements should not contain a
150            leading '/'.
151
152        """
153        self._root_directories += directories
154
155
156    def add_startup_command(self, command):
157        """Add a command to the script run when the chroot starts up.
158
159        @param command string containing the command line to run.
160
161        """
162        self._config_file_templates[self.STARTUP] += '%s\n' % command
163
164
165    def add_environment(self, env_dict):
166        """Add variables to the chroot environment.
167
168        @param env_dict dict dictionary containing environment variables
169        """
170        self._env.update(env_dict)
171
172
173    def get_log_contents(self):
174        """Return the logfiles from the chroot."""
175        return utils.system_output("head -10000 %s" %
176                                   self.chroot_path("var/log/*"))
177
178
179    def bridge_dbus_namespaces(self):
180        """Make the system DBus daemon visible inside the chroot."""
181        # Need the system socket and the machine-id.
182        self._bind_root_directories += self.DBUS_BRIDGE_DIRECTORIES
183
184
185    def chroot_path(self, path):
186        """Returns the the path within the chroot for |path|.
187
188        @param path string filename within the choot.  This should not
189            contain a leading '/'.
190
191        """
192        return os.path.join(self._temp_dir, path.lstrip('/'))
193
194
195    def get_pid_file(self, pid_file, missing_ok=False):
196        """Returns the integer contents of |pid_file| in the chroot.
197
198        @param pid_file string containing the filename within the choot
199            to read and convert to an integer.  This should not contain a
200            leading '/'.
201        @param missing_ok bool indicating whether exceptions due to failure
202            to open the pid file should be caught.  If true a missing pid
203            file will cause this method to return 0.  If false, a missing
204            pid file will cause an exception.
205
206        """
207        chroot_pid_file = self.chroot_path(pid_file)
208        try:
209            with open(chroot_pid_file) as f:
210                return int(f.read())
211        except IOError, e:
212            if not missing_ok or e.errno != errno.ENOENT:
213                raise e
214
215            return 0
216
217
218    def kill_pid_file(self, pid_file, missing_ok=False):
219        """Kills the process belonging to |pid_file| in the chroot.
220
221        @param pid_file string filename within the chroot to gain the process ID
222            which this method will kill.
223        @param missing_ok bool indicating whether a missing pid file is okay,
224            and should be ignored.
225
226        """
227        pid = self.get_pid_file(pid_file, missing_ok=missing_ok)
228        if missing_ok and pid == 0:
229            return
230        utils.system('kill %d' % pid, ignore_status=True)
231
232
233    def make_chroot(self):
234        """Make a chroot filesystem."""
235        self._temp_dir = utils.system_output(
236                'mktemp -d /usr/local/tmp/chroot.XXXXXXXXX')
237        utils.system('chmod go+rX %s' % self._temp_dir)
238        for rootdir in self._root_directories:
239            os.mkdir(self.chroot_path(rootdir))
240
241        self._jail_args = []
242        for rootdir in self._bind_root_directories:
243            src_path = os.path.join('/', rootdir)
244            dst_path = self.chroot_path(rootdir)
245            if not os.path.exists(src_path):
246                continue
247            elif os.path.islink(src_path):
248                link_path = os.readlink(src_path)
249                os.symlink(link_path, dst_path)
250            else:
251                os.makedirs(dst_path)  # Recursively create directories.
252                mount_arg = '%s,%s' % (src_path, src_path)
253                if rootdir in self.BIND_ROOT_WRITABLE_DIRECTORIES:
254                    mount_arg += ',1'
255                self._jail_args += [ '-b', mount_arg ]
256
257        for config_file in self._copied_config_files:
258            src_path = os.path.join('/', config_file)
259            dst_path = self.chroot_path(config_file)
260            if os.path.exists(src_path):
261                shutil.copyfile(src_path, dst_path)
262
263        for src_path, target_path in self.ROOT_SYMLINKS:
264            link_path = self.chroot_path(src_path)
265            os.symlink(target_path, link_path)
266
267
268    def move_interface_to_chroot_namespace(self):
269        """Move network interface to the network namespace of the server."""
270        utils.system('ip link set %s netns %d' %
271                     (self._interface,
272                      self.get_pid_file(self.STARTUP_PID_FILE)))
273
274
275    def run(self, args, ignore_status=False):
276        """Run a command in a chroot, within a separate network namespace.
277
278        @param args list containing the command line arguments to run.
279        @param ignore_status bool set to true if a failure should be ignored.
280
281        """
282        utils.run('minijail0 -e -C %s %s' %
283                  (self._temp_dir, ' '.join(self._jail_args + args)),
284                  timeout=None,
285                  ignore_status=ignore_status,
286                  stdout_tee=utils.TEE_TO_LOGS,
287                  stderr_tee=utils.TEE_TO_LOGS,
288                  env=self._env)
289
290
291    def write_configs(self):
292        """Write out config files"""
293        for config_file, template in self._config_file_templates.iteritems():
294            with open(self.chroot_path(config_file), 'w') as f:
295                f.write(template % self._config_file_values)
296