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