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