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