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