network_chroot.py revision 436adf9c1b9404697da462e0048ac6d2cf046554
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 - Manually mount /dev/pts, since the bind mount that the 29 minijail0 command creates is read-only and xl2tpd is unable 30 to perform operations like chown() on it which is a fatal error. 31 - We can now configure the network interface with an address. 32 - At this point, we can now start any user-requested server 33 processes. 34 """ 35 BIND_ROOT_DIRECTORIES = ('bin', 'dev', 'lib', 'lib32', 'lib64', 36 'proc', 'sbin', 'sys', 'usr', 'usr/local') 37 ROOT_DIRECTORIES = ('etc', 'tmp', 'var', 'var/log', 'var/run') 38 STARTUP = 'etc/chroot_startup.sh' 39 STARTUP_DELAY_SECONDS = 5 40 STARTUP_PID_FILE = 'var/run/vpn_startup.pid' 41 STARTUP_SLEEPER_PID_FILE = 'var/run/vpn_sleeper.pid' 42 43 # These flags must track those given at boot in /sbin/chromeos_startup, 44 # otherwise we will end up changing the mount flags for the root 45 # filesystem's /dev/pts. 46 DEV_PTS_FLAGS = 'noexec,nosuid,gid=5,mode=0620' 47 48 COPIED_CONFIG_FILES = [ 49 'etc/ld.so.cache' 50 ] 51 CONFIG_FILE_TEMPLATES = { 52 STARTUP: 53 '#!/bin/sh\n' 54 'exec > /var/log/startup.log 2>&1\n' 55 'set -x\n' 56 'echo $$ > /%(startup-pidfile)s\n' 57 'sleep %(startup-delay-seconds)d &\n' 58 'echo $! > /%(sleeper-pidfile)s &\n' 59 'wait\n' 60 'mount -t devpts -o %(dev-pts-flags)s devpts /dev/pts\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 'dev-pts-flags': DEV_PTS_FLAGS, 66 'sleeper-pidfile': STARTUP_SLEEPER_PID_FILE, 67 'startup-delay-seconds': STARTUP_DELAY_SECONDS, 68 'startup-pidfile': STARTUP_PID_FILE 69 } 70 71 def __init__(self, interface, address, prefix): 72 self._interface = interface 73 74 # Copy these values from the class-static since specific instances 75 # of this class are allowed to modify their contents. 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_root_directories(self, directories): 128 """Add |directories| to the set created within the chroot. 129 130 @param directories list/tuple containing a list of directories to 131 be created in the chroot. These elements should not contain a 132 leading '/'. 133 134 """ 135 self._root_directories += directories 136 137 138 def add_startup_command(self, command): 139 """Add a command to the script run when the chroot starts up. 140 141 @param command string containing the command line to run. 142 143 """ 144 self._config_file_templates[self.STARTUP] += '%s\n' % command 145 146 147 def get_log_contents(self): 148 """Return the logfiles from the chroot.""" 149 return utils.system_output("head -10000 %s" % 150 self.chroot_path("var/log/*")) 151 152 153 def chroot_path(self, path): 154 """Returns the the path within the chroot for |path|. 155 156 @param path string filename within the choot. This should not 157 contain a leading '/'. 158 159 """ 160 return os.path.join(self._temp_dir, path.lstrip('/')) 161 162 163 def get_pid_file(self, pid_file, missing_ok=False): 164 """Returns the integer contents of |pid_file| in the chroot. 165 166 @param pid_file string containing the filename within the choot 167 to read and convert to an integer. This should not contain a 168 leading '/'. 169 @param missing_ok bool indicating whether exceptions due to failure 170 to open the pid file should be caught. If true a missing pid 171 file will cause this method to return 0. If false, a missing 172 pid file will cause an exception. 173 174 """ 175 chroot_pid_file = self.chroot_path(pid_file) 176 try: 177 with open(chroot_pid_file) as f: 178 return int(f.read()) 179 except IOError, e: 180 if not missing_ok or e.errno != errno.ENOENT: 181 raise e 182 183 return 0 184 185 186 def kill_pid_file(self, pid_file, missing_ok=False): 187 """Kills the process belonging to |pid_file| in the chroot. 188 189 @param pid_file string filename within the chroot to gain the process ID 190 which this method will kill. 191 @param missing_ok bool indicating whether a missing pid file is okay, 192 and should be ignored. 193 194 """ 195 pid = self.get_pid_file(pid_file, missing_ok) 196 if missing_ok and pid == 0: 197 return 198 utils.system('kill %d' % pid, ignore_status=True) 199 200 201 def make_chroot(self): 202 """Make a chroot filesystem.""" 203 self._temp_dir = utils.system_output('mktemp -d /tmp/chroot.XXXXXXXXX') 204 utils.system('chmod go+rX %s' % self._temp_dir) 205 for rootdir in self._root_directories: 206 os.mkdir(self.chroot_path(rootdir)) 207 208 self._jail_args = [] 209 for rootdir in self.BIND_ROOT_DIRECTORIES: 210 src_path = os.path.join('/', rootdir) 211 dst_path = self.chroot_path(rootdir) 212 if not os.path.exists(src_path): 213 continue 214 elif os.path.islink(src_path): 215 link_path = os.readlink(src_path) 216 os.symlink(link_path, dst_path) 217 else: 218 os.mkdir(dst_path) 219 self._jail_args += [ '-b', '%s,%s' % (src_path, src_path) ] 220 221 for config_file in self._copied_config_files: 222 src_path = os.path.join('/', config_file) 223 dst_path = self.chroot_path(config_file) 224 if os.path.exists(src_path): 225 shutil.copyfile(src_path, dst_path) 226 227 228 def move_interface_to_chroot_namespace(self): 229 """Move network interface to the network namespace of the server.""" 230 utils.system('ip link set %s netns %d' % 231 (self._interface, 232 self.get_pid_file(self.STARTUP_PID_FILE))) 233 234 235 def run(self, args): 236 """Run a command in a chroot, within a separate network namespace. 237 238 @param args list containing the command line arguments to run. 239 240 """ 241 utils.system('minijail0 -e -C %s %s' % 242 (self._temp_dir, ' '.join(self._jail_args + args))) 243 244 245 def write_configs(self): 246 """Write out config files""" 247 for config_file, template in self._config_file_templates.iteritems(): 248 with open(self.chroot_path(config_file), 'w') as f: 249 f.write(template % self._config_file_values) 250