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