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