1# Lint as: python2, python3 2# Copyright (c) 2013 The Chromium OS Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6from __future__ import absolute_import 7from __future__ import division 8from __future__ import print_function 9 10import errno 11import os 12import shutil 13import six 14import time 15 16from autotest_lib.client.bin import utils 17 18class NetworkChroot(object): 19 """Implements a chroot environment that runs in a separate network 20 namespace from the caller. This is useful for network tests that 21 involve creating a server on the other end of a virtual ethernet 22 pair. This object is initialized with an interface name to pass 23 to the chroot, as well as the IP address to assign to this 24 interface, since in passing the interface into the chroot, any 25 pre-configured address is removed. 26 27 The startup of the chroot is an orchestrated process where a 28 small startup script is run to perform the following tasks: 29 - Write out pid file which will be a handle to the 30 network namespace that that |interface| should be passed to. 31 - Wait for the network namespace to be passed in, by performing 32 a "sleep" and writing the pid of this process as well. Our 33 parent will kill this process to resume the startup process. 34 - We can now configure the network interface with an address. 35 - At this point, we can now start any user-requested server 36 processes. 37 """ 38 BIND_ROOT_DIRECTORIES = ('bin', 'dev', 'dev/pts', 'lib', 'lib32', 'lib64', 39 'proc', 'sbin', 'sys', 'usr', 'usr/local') 40 # Subset of BIND_ROOT_DIRECTORIES that should be mounted writable. 41 BIND_ROOT_WRITABLE_DIRECTORIES = frozenset(('dev/pts',)) 42 # Directories we'll bind mount when we want to bridge DBus namespaces. 43 # Includes directories containing the system bus socket and machine ID. 44 DBUS_BRIDGE_DIRECTORIES = ('run/dbus/', 'var/lib/dbus/') 45 46 ROOT_DIRECTORIES = ('etc', 'etc/ssl', 'tmp', 'var', 'var/log', 'run', 47 'run/lock') 48 ROOT_SYMLINKS = ( 49 ('var/run', '/run'), 50 ('var/lock', '/run/lock'), 51 ) 52 STARTUP = 'etc/chroot_startup.sh' 53 STARTUP_DELAY_SECONDS = 5 54 STARTUP_PID_FILE = 'run/vpn_startup.pid' 55 STARTUP_SLEEPER_PID_FILE = 'run/vpn_sleeper.pid' 56 COPIED_CONFIG_FILES = [ 57 'etc/ld.so.cache', 58 'etc/ssl/openssl.cnf.compat' 59 ] 60 CONFIG_FILE_TEMPLATES = { 61 STARTUP: 62 '#!/bin/sh\n' 63 'exec > /var/log/startup.log 2>&1\n' 64 'set -x\n' 65 'echo $$ > /%(startup-pidfile)s\n' 66 'sleep %(startup-delay-seconds)d &\n' 67 'echo $! > /%(sleeper-pidfile)s &\n' 68 'wait\n' 69 'ip addr add %(local-ip-and-prefix)s dev %(local-interface-name)s\n' 70 'ip link set %(local-interface-name)s up\n' 71 # For running strongSwan VPN with flag --with-piddir=/run/ipsec. We 72 # want to use /run/ipsec for strongSwan runtime data dir instead of 73 # /run, and the cmdline flag applies to both client and server. 74 'mkdir -p /run/ipsec\n' 75 } 76 CONFIG_FILE_VALUES = { 77 'sleeper-pidfile': STARTUP_SLEEPER_PID_FILE, 78 'startup-delay-seconds': STARTUP_DELAY_SECONDS, 79 'startup-pidfile': STARTUP_PID_FILE 80 } 81 82 def __init__(self, interface, address, prefix): 83 self._interface = interface 84 85 # Copy these values from the class-static since specific instances 86 # of this class are allowed to modify their contents. 87 self._bind_root_directories = list(self.BIND_ROOT_DIRECTORIES) 88 self._root_directories = list(self.ROOT_DIRECTORIES) 89 self._copied_config_files = list(self.COPIED_CONFIG_FILES) 90 self._config_file_templates = self.CONFIG_FILE_TEMPLATES.copy() 91 self._config_file_values = self.CONFIG_FILE_VALUES.copy() 92 self._env = dict(os.environ) 93 94 self._config_file_values.update({ 95 'local-interface-name': interface, 96 'local-ip': address, 97 'local-ip-and-prefix': '%s/%d' % (address, prefix) 98 }) 99 100 101 def startup(self): 102 """Create the chroot and start user processes.""" 103 self.make_chroot() 104 self.write_configs() 105 self.run(['/bin/bash', os.path.join('/', self.STARTUP), '&']) 106 self.move_interface_to_chroot_namespace() 107 self.kill_pid_file(self.STARTUP_SLEEPER_PID_FILE) 108 109 110 def shutdown(self): 111 """Remove the chroot filesystem in which the VPN server was running""" 112 # TODO(pstew): Some processes take a while to exit, which will cause 113 # the cleanup below to fail to complete successfully... 114 time.sleep(10) 115 utils.system_output('rm -rf --one-file-system %s' % self._temp_dir, 116 ignore_status=True) 117 118 119 def add_config_templates(self, template_dict): 120 """Add a filename-content dict to the set of templates for the chroot 121 122 @param template_dict dict containing filename-content pairs for 123 templates to be applied to the chroot. The keys to this dict 124 should not contain a leading '/'. 125 126 """ 127 self._config_file_templates.update(template_dict) 128 129 130 def add_config_values(self, value_dict): 131 """Add a name-value dict to the set of values for the config template 132 133 @param value_dict dict containing key-value pairs of values that will 134 be applied to the config file templates. 135 136 """ 137 self._config_file_values.update(value_dict) 138 139 140 def add_copied_config_files(self, files): 141 """Add |files| to the set to be copied to the chroot. 142 143 @param files iterable object containing a list of files to 144 be copied into the chroot. These elements should not contain a 145 leading '/'. 146 147 """ 148 self._copied_config_files += files 149 150 151 def add_root_directories(self, directories): 152 """Add |directories| to the set created within the chroot. 153 154 @param directories list/tuple containing a list of directories to 155 be created in the chroot. These elements should not contain a 156 leading '/'. 157 158 """ 159 self._root_directories += directories 160 161 162 def add_startup_command(self, command): 163 """Add a command to the script run when the chroot starts up. 164 165 @param command string containing the command line to run. 166 167 """ 168 self._config_file_templates[self.STARTUP] += '%s\n' % command 169 170 171 def add_environment(self, env_dict): 172 """Add variables to the chroot environment. 173 174 @param env_dict dict dictionary containing environment variables 175 """ 176 self._env.update(env_dict) 177 178 179 def get_log_contents(self): 180 """Return the logfiles from the chroot.""" 181 return utils.system_output("head -10000 %s" % 182 self.chroot_path("var/log/*")) 183 184 185 def bridge_dbus_namespaces(self): 186 """Make the system DBus daemon visible inside the chroot.""" 187 # Need the system socket and the machine-id. 188 self._bind_root_directories += self.DBUS_BRIDGE_DIRECTORIES 189 190 191 def chroot_path(self, path): 192 """Returns the the path within the chroot for |path|. 193 194 @param path string filename within the choot. This should not 195 contain a leading '/'. 196 197 """ 198 return os.path.join(self._temp_dir, path.lstrip('/')) 199 200 201 def get_pid_file(self, pid_file, missing_ok=False): 202 """Returns the integer contents of |pid_file| in the chroot. 203 204 @param pid_file string containing the filename within the choot 205 to read and convert to an integer. This should not contain a 206 leading '/'. 207 @param missing_ok bool indicating whether exceptions due to failure 208 to open the pid file should be caught. If true a missing pid 209 file will cause this method to return 0. If false, a missing 210 pid file will cause an exception. 211 212 """ 213 chroot_pid_file = self.chroot_path(pid_file) 214 try: 215 with open(chroot_pid_file) as f: 216 return int(f.read()) 217 except IOError as e: 218 if not missing_ok or e.errno != errno.ENOENT: 219 raise e 220 221 return 0 222 223 224 def kill_pid_file(self, pid_file, missing_ok=False): 225 """Kills the process belonging to |pid_file| in the chroot. 226 227 @param pid_file string filename within the chroot to gain the process ID 228 which this method will kill. 229 @param missing_ok bool indicating whether a missing pid file is okay, 230 and should be ignored. 231 232 """ 233 pid = self.get_pid_file(pid_file, missing_ok=missing_ok) 234 if missing_ok and pid == 0: 235 return 236 utils.system('kill %d' % pid, ignore_status=True) 237 238 239 def make_chroot(self): 240 """Make a chroot filesystem.""" 241 self._temp_dir = utils.system_output( 242 'mktemp -d /usr/local/tmp/chroot.XXXXXXXXX') 243 utils.system('chmod go+rX %s' % self._temp_dir) 244 for rootdir in self._root_directories: 245 os.mkdir(self.chroot_path(rootdir)) 246 247 self._jail_args = [] 248 for rootdir in self._bind_root_directories: 249 src_path = os.path.join('/', rootdir) 250 dst_path = self.chroot_path(rootdir) 251 if not os.path.exists(src_path): 252 continue 253 elif os.path.islink(src_path): 254 link_path = os.readlink(src_path) 255 os.symlink(link_path, dst_path) 256 else: 257 os.makedirs(dst_path) # Recursively create directories. 258 mount_arg = '%s,%s' % (src_path, src_path) 259 if rootdir in self.BIND_ROOT_WRITABLE_DIRECTORIES: 260 mount_arg += ',1' 261 self._jail_args += [ '-b', mount_arg ] 262 263 for config_file in self._copied_config_files: 264 src_path = os.path.join('/', config_file) 265 dst_path = self.chroot_path(config_file) 266 if os.path.exists(src_path): 267 shutil.copyfile(src_path, dst_path) 268 269 for src_path, target_path in self.ROOT_SYMLINKS: 270 link_path = self.chroot_path(src_path) 271 os.symlink(target_path, link_path) 272 273 274 def move_interface_to_chroot_namespace(self): 275 """Move network interface to the network namespace of the server.""" 276 utils.system('ip link set %s netns %d' % 277 (self._interface, 278 self.get_pid_file(self.STARTUP_PID_FILE))) 279 280 281 def run(self, args, ignore_status=False): 282 """Run a command in a chroot, within a separate network namespace. 283 284 @param args list containing the command line arguments to run. 285 @param ignore_status bool set to true if a failure should be ignored. 286 287 """ 288 utils.run('minijail0 -e -C %s %s' % 289 (self._temp_dir, ' '.join(self._jail_args + args)), 290 timeout=None, 291 ignore_status=ignore_status, 292 stdout_tee=utils.TEE_TO_LOGS, 293 stderr_tee=utils.TEE_TO_LOGS, 294 env=self._env) 295 296 297 def write_configs(self): 298 """Write out config files""" 299 for config_file, template in six.iteritems(self._config_file_templates): 300 with open(self.chroot_path(config_file), 'w') as f: 301 f.write(template % self._config_file_values) 302