302 lines
11 KiB
Python
302 lines
11 KiB
Python
# 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)
|