|  | # Lint as: python2, python3 | 
|  | # 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. | 
|  |  | 
|  | from __future__ import absolute_import | 
|  | from __future__ import division | 
|  | from __future__ import print_function | 
|  |  | 
|  | import errno | 
|  | import os | 
|  | import shutil | 
|  | import six | 
|  | 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', 'etc/ssl', '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', | 
|  | 'etc/ssl/openssl.cnf.compat' | 
|  | ] | 
|  | 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._env = dict(os.environ) | 
|  |  | 
|  | 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 add_environment(self, env_dict): | 
|  | """Add variables to the chroot environment. | 
|  |  | 
|  | @param env_dict dict dictionary containing environment variables | 
|  | """ | 
|  | self._env.update(env_dict) | 
|  |  | 
|  |  | 
|  | 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 as 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 /usr/local/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.run('minijail0 -e -C %s %s' % | 
|  | (self._temp_dir, ' '.join(self._jail_args + args)), | 
|  | timeout=None, | 
|  | ignore_status=ignore_status, | 
|  | stdout_tee=utils.TEE_TO_LOGS, | 
|  | stderr_tee=utils.TEE_TO_LOGS, | 
|  | env=self._env) | 
|  |  | 
|  |  | 
|  | def write_configs(self): | 
|  | """Write out config files""" | 
|  | for config_file, template in six.iteritems(self._config_file_templates): | 
|  | with open(self.chroot_path(config_file), 'w') as f: | 
|  | f.write(template % self._config_file_values) |