| # Copyright (c) 2013 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import errno |
| import os |
| import shutil |
| import time |
| |
| from autotest_lib.client.bin import utils |
| |
| class NetworkChroot(object): |
| """Implements a chroot environment that runs in a separate network |
| namespace from the caller. This is useful for network tests that |
| involve creating a server on the other end of a virtual ethernet |
| pair. This object is initialized with an interface name to pass |
| to the chroot, as well as the IP address to assign to this |
| interface, since in passing the interface into the chroot, any |
| pre-configured address is removed. |
| |
| The startup of the chroot is an orchestrated process where a |
| small startup script is run to perform the following tasks: |
| - Write out pid file which will be a handle to the |
| network namespace that that |interface| should be passed to. |
| - Wait for the network namespace to be passed in, by performing |
| a "sleep" and writing the pid of this process as well. Our |
| parent will kill this process to resume the startup process. |
| - We can now configure the network interface with an address. |
| - At this point, we can now start any user-requested server |
| processes. |
| """ |
| BIND_ROOT_DIRECTORIES = ('bin', 'dev', 'dev/pts', 'lib', 'lib32', 'lib64', |
| 'proc', 'sbin', 'sys', 'usr', 'usr/local') |
| # Subset of BIND_ROOT_DIRECTORIES that should be mounted writable. |
| BIND_ROOT_WRITABLE_DIRECTORIES = frozenset(('dev/pts',)) |
| # Directories we'll bind mount when we want to bridge DBus namespaces. |
| # Includes directories containing the system bus socket and machine ID. |
| DBUS_BRIDGE_DIRECTORIES = ('run/dbus/', 'var/lib/dbus/') |
| |
| ROOT_DIRECTORIES = ('etc', 'tmp', 'var', 'var/log', 'run', 'run/lock') |
| ROOT_SYMLINKS = ( |
| ('var/run', '/run'), |
| ('var/lock', '/run/lock'), |
| ) |
| STARTUP = 'etc/chroot_startup.sh' |
| STARTUP_DELAY_SECONDS = 5 |
| STARTUP_PID_FILE = 'run/vpn_startup.pid' |
| STARTUP_SLEEPER_PID_FILE = 'run/vpn_sleeper.pid' |
| COPIED_CONFIG_FILES = [ |
| 'etc/ld.so.cache' |
| ] |
| CONFIG_FILE_TEMPLATES = { |
| STARTUP: |
| '#!/bin/sh\n' |
| 'exec > /var/log/startup.log 2>&1\n' |
| 'set -x\n' |
| 'echo $$ > /%(startup-pidfile)s\n' |
| 'sleep %(startup-delay-seconds)d &\n' |
| 'echo $! > /%(sleeper-pidfile)s &\n' |
| 'wait\n' |
| 'ip addr add %(local-ip-and-prefix)s dev %(local-interface-name)s\n' |
| 'ip link set %(local-interface-name)s up\n' |
| # For running strongSwan VPN with flag --with-piddir=/run/ipsec. We |
| # want to use /run/ipsec for strongSwan runtime data dir instead of |
| # /run, and the cmdline flag applies to both client and server. |
| 'mkdir -p /run/ipsec\n' |
| } |
| CONFIG_FILE_VALUES = { |
| 'sleeper-pidfile': STARTUP_SLEEPER_PID_FILE, |
| 'startup-delay-seconds': STARTUP_DELAY_SECONDS, |
| 'startup-pidfile': STARTUP_PID_FILE |
| } |
| |
| def __init__(self, interface, address, prefix): |
| self._interface = interface |
| |
| # Copy these values from the class-static since specific instances |
| # of this class are allowed to modify their contents. |
| self._bind_root_directories = list(self.BIND_ROOT_DIRECTORIES) |
| self._root_directories = list(self.ROOT_DIRECTORIES) |
| self._copied_config_files = list(self.COPIED_CONFIG_FILES) |
| self._config_file_templates = self.CONFIG_FILE_TEMPLATES.copy() |
| self._config_file_values = self.CONFIG_FILE_VALUES.copy() |
| |
| self._config_file_values.update({ |
| 'local-interface-name': interface, |
| 'local-ip': address, |
| 'local-ip-and-prefix': '%s/%d' % (address, prefix) |
| }) |
| |
| |
| def startup(self): |
| """Create the chroot and start user processes.""" |
| self.make_chroot() |
| self.write_configs() |
| self.run(['/bin/bash', os.path.join('/', self.STARTUP), '&']) |
| self.move_interface_to_chroot_namespace() |
| self.kill_pid_file(self.STARTUP_SLEEPER_PID_FILE) |
| |
| |
| def shutdown(self): |
| """Remove the chroot filesystem in which the VPN server was running""" |
| # TODO(pstew): Some processes take a while to exit, which will cause |
| # the cleanup below to fail to complete successfully... |
| time.sleep(10) |
| utils.system_output('rm -rf --one-file-system %s' % self._temp_dir, |
| ignore_status=True) |
| |
| |
| def add_config_templates(self, template_dict): |
| """Add a filename-content dict to the set of templates for the chroot |
| |
| @param template_dict dict containing filename-content pairs for |
| templates to be applied to the chroot. The keys to this dict |
| should not contain a leading '/'. |
| |
| """ |
| self._config_file_templates.update(template_dict) |
| |
| |
| def add_config_values(self, value_dict): |
| """Add a name-value dict to the set of values for the config template |
| |
| @param value_dict dict containing key-value pairs of values that will |
| be applied to the config file templates. |
| |
| """ |
| self._config_file_values.update(value_dict) |
| |
| |
| def add_copied_config_files(self, files): |
| """Add |files| to the set to be copied to the chroot. |
| |
| @param files iterable object containing a list of files to |
| be copied into the chroot. These elements should not contain a |
| leading '/'. |
| |
| """ |
| self._copied_config_files += files |
| |
| |
| def add_root_directories(self, directories): |
| """Add |directories| to the set created within the chroot. |
| |
| @param directories list/tuple containing a list of directories to |
| be created in the chroot. These elements should not contain a |
| leading '/'. |
| |
| """ |
| self._root_directories += directories |
| |
| |
| def add_startup_command(self, command): |
| """Add a command to the script run when the chroot starts up. |
| |
| @param command string containing the command line to run. |
| |
| """ |
| self._config_file_templates[self.STARTUP] += '%s\n' % command |
| |
| |
| def get_log_contents(self): |
| """Return the logfiles from the chroot.""" |
| return utils.system_output("head -10000 %s" % |
| self.chroot_path("var/log/*")) |
| |
| |
| def bridge_dbus_namespaces(self): |
| """Make the system DBus daemon visible inside the chroot.""" |
| # Need the system socket and the machine-id. |
| self._bind_root_directories += self.DBUS_BRIDGE_DIRECTORIES |
| |
| |
| def chroot_path(self, path): |
| """Returns the the path within the chroot for |path|. |
| |
| @param path string filename within the choot. This should not |
| contain a leading '/'. |
| |
| """ |
| return os.path.join(self._temp_dir, path.lstrip('/')) |
| |
| |
| def get_pid_file(self, pid_file, missing_ok=False): |
| """Returns the integer contents of |pid_file| in the chroot. |
| |
| @param pid_file string containing the filename within the choot |
| to read and convert to an integer. This should not contain a |
| leading '/'. |
| @param missing_ok bool indicating whether exceptions due to failure |
| to open the pid file should be caught. If true a missing pid |
| file will cause this method to return 0. If false, a missing |
| pid file will cause an exception. |
| |
| """ |
| chroot_pid_file = self.chroot_path(pid_file) |
| try: |
| with open(chroot_pid_file) as f: |
| return int(f.read()) |
| except IOError, e: |
| if not missing_ok or e.errno != errno.ENOENT: |
| raise e |
| |
| return 0 |
| |
| |
| def kill_pid_file(self, pid_file, missing_ok=False): |
| """Kills the process belonging to |pid_file| in the chroot. |
| |
| @param pid_file string filename within the chroot to gain the process ID |
| which this method will kill. |
| @param missing_ok bool indicating whether a missing pid file is okay, |
| and should be ignored. |
| |
| """ |
| pid = self.get_pid_file(pid_file, missing_ok=missing_ok) |
| if missing_ok and pid == 0: |
| return |
| utils.system('kill %d' % pid, ignore_status=True) |
| |
| |
| def make_chroot(self): |
| """Make a chroot filesystem.""" |
| self._temp_dir = utils.system_output('mktemp -d /tmp/chroot.XXXXXXXXX') |
| utils.system('chmod go+rX %s' % self._temp_dir) |
| for rootdir in self._root_directories: |
| os.mkdir(self.chroot_path(rootdir)) |
| |
| self._jail_args = [] |
| for rootdir in self._bind_root_directories: |
| src_path = os.path.join('/', rootdir) |
| dst_path = self.chroot_path(rootdir) |
| if not os.path.exists(src_path): |
| continue |
| elif os.path.islink(src_path): |
| link_path = os.readlink(src_path) |
| os.symlink(link_path, dst_path) |
| else: |
| os.makedirs(dst_path) # Recursively create directories. |
| mount_arg = '%s,%s' % (src_path, src_path) |
| if rootdir in self.BIND_ROOT_WRITABLE_DIRECTORIES: |
| mount_arg += ',1' |
| self._jail_args += [ '-b', mount_arg ] |
| |
| for config_file in self._copied_config_files: |
| src_path = os.path.join('/', config_file) |
| dst_path = self.chroot_path(config_file) |
| if os.path.exists(src_path): |
| shutil.copyfile(src_path, dst_path) |
| |
| for src_path, target_path in self.ROOT_SYMLINKS: |
| link_path = self.chroot_path(src_path) |
| os.symlink(target_path, link_path) |
| |
| |
| def move_interface_to_chroot_namespace(self): |
| """Move network interface to the network namespace of the server.""" |
| utils.system('ip link set %s netns %d' % |
| (self._interface, |
| self.get_pid_file(self.STARTUP_PID_FILE))) |
| |
| |
| def run(self, args, ignore_status=False): |
| """Run a command in a chroot, within a separate network namespace. |
| |
| @param args list containing the command line arguments to run. |
| @param ignore_status bool set to true if a failure should be ignored. |
| |
| """ |
| utils.system('minijail0 -e -C %s %s' % |
| (self._temp_dir, ' '.join(self._jail_args + args)), |
| ignore_status=ignore_status) |
| |
| |
| def write_configs(self): |
| """Write out config files""" |
| for config_file, template in self._config_file_templates.iteritems(): |
| with open(self.chroot_path(config_file), 'w') as f: |
| f.write(template % self._config_file_values) |