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