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 5 import errno 6 import os 7 import shutil 8 import time 9 10 from autotest_lib.client.bin import utils 11 12 class 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