summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSam Thursfield <sam.thursfield@codethink.co.uk>2015-06-09 15:27:17 +0100
committerSam Thursfield <sam.thursfield@codethink.co.uk>2015-06-09 15:27:17 +0100
commit2e588565656d82689fed541a690d2c9a2e545dc1 (patch)
tree507000bfc47d868b2848a84d659c0c73d4d5d72c
parentb814d929ffba615faab7e7cbd09ecb4a01e2f50c (diff)
parente961f82127860d8efd3cfd9aa10f2cd4820d74f6 (diff)
downloadsandboxlib-2e588565656d82689fed541a690d2c9a2e545dc1.tar.gz
Merge branch '0.3.0'0.3.0
-rw-r--r--sandboxlib/__init__.py34
-rw-r--r--sandboxlib/chroot.py54
-rw-r--r--sandboxlib/linux_user_chroot.py34
-rw-r--r--sandboxlib/utils.py8
-rw-r--r--tests/test_all.py34
5 files changed, 100 insertions, 64 deletions
diff --git a/sandboxlib/__init__.py b/sandboxlib/__init__.py
index c568357..8179d72 100644
--- a/sandboxlib/__init__.py
+++ b/sandboxlib/__init__.py
@@ -33,30 +33,18 @@ class ProgramNotFound(Exception):
pass
-def maximum_possible_isolation():
- '''Describe the 'tightest' isolation possible with a specific backend.
+def degrade_config_for_capabilities(in_config, warn=True):
+ '''Alter settings in 'in_config' that a given backend doesn't support.
- This function returns a dict, with the following keys:
+ This function is provided for users who want to be flexible about which
+ sandbox implementation they use, and who don't mind if not all of the
+ isolation that they requested is actually possible.
- - mounts
- - network
+ This is not a general purpose 'check your config' function. Any unexpected
+ keys or values in ``in_config`` will just be ignored.
- Each key maps to a parameter of the run_sandbox() function, and each
- value is a valid value for that parameter.
-
- Example result:
-
- {
- 'mounts': 'undefined'
- 'network': 'isolated'
- }
-
- You can pass the result directly to a run_sandbox() function directly,
- using the `**` operator to turn it into keyword arguments as in the
- following example:
-
- isolation_settings = maximum_possible_isolation()
- run_sandbox(root_path, ['echo', 'hello'], **isolation_settings)
+ If 'warn' is True, each change the function makes is logged using
+ warnings.warn().
'''
raise NotImplementedError()
@@ -135,14 +123,14 @@ def run_sandbox_with_redirection(command, **sandbox_config):
raise NotImplementedError()
-def sandbox_module_for_platform():
+def executor_for_platform():
'''Returns an execution module that will work on the current platform.'''
log = logging.getLogger("sandboxlib")
backend = None
- if platform.uname() == 'Linux':
+ if platform.uname()[0] == 'Linux':
log.info("Linux detected, looking for 'linux-user-chroot'.")
try:
program = sandboxlib.linux_user_chroot.linux_user_chroot_program()
diff --git a/sandboxlib/chroot.py b/sandboxlib/chroot.py
index 9f7b16a..71e45e4 100644
--- a/sandboxlib/chroot.py
+++ b/sandboxlib/chroot.py
@@ -18,16 +18,12 @@
This implements an API defined in sandboxlib/__init__.py.
This backend should work on any POSIX-compliant operating system. It has been
-tested on Linux only. The calling process must be able to use the chroot()
-syscall, which is likely to require 'root' priviliges.
+tested on Linux and Mac OS X. The calling process must be able to use the
+chroot() syscall, which is likely to require 'root' priviliges.
If any 'extra_mounts' are specified, there must be a working 'mount' binary in
the host system.
-Supported mounts settings: 'undefined'.
-
-Supported network settings: 'undefined'.
-
The code would be simpler if we just used the 'chroot' program, but it's not
always practical to do that. First, it may not be installed. Second, we can't
set the working directory of the program inside the chroot, unless we assume
@@ -46,17 +42,42 @@ import warnings
import sandboxlib
-def maximum_possible_isolation():
- return {
- 'mounts': 'undefined',
- 'network': 'undefined',
- }
+CAPABILITIES = {
+ 'network': ['undefined'],
+ 'mounts': ['undefined'],
+ 'writable_paths': ['all'],
+}
-def process_mount_config(mounts, extra_mounts):
- supported_values = ['undefined', 'isolated']
+def degrade_config_for_capabilities(in_config, warn=True):
+ # Currently this is all done manually... it may make sense to add something
+ # in utils.py that automatically checks the config against CAPABILITIES.
+ out_config = in_config.copy()
+
+ def degrade_and_warn(name, allowed_value):
+ if warn:
+ backend = 'chroot'
+ value = out_config[name]
+ msg = (
+ 'Unable to set %(name)s=%(value)s in a %(backend)s sandbox, '
+ 'falling back to %(name)s=%(allowed_value)s' % locals())
+ warnings.warn(msg)
+ out_config[name] = allowed_value
+
+ if out_config.get('mounts', 'undefined') != 'undefined':
+ degrade_and_warn('mounts', 'undefined')
+
+ if out_config.get('network', 'undefined') != 'undefined':
+ degrade_and_warn('network', 'undefined')
- assert mounts in supported_values, \
+ if out_config.get('filesystem_writable_paths', 'all') != 'all':
+ degrade_and_warn('filesystem_writable_paths', 'all')
+
+ return out_config
+
+
+def process_mount_config(mounts, extra_mounts):
+ assert mounts == 'undefined', \
"'%s' is an unsupported value for 'mounts' in the 'chroot' " \
"Mount sharing cannot be configured in this backend." % mounts
@@ -66,11 +87,6 @@ def process_mount_config(mounts, extra_mounts):
def process_network_config(network):
- # It'd be possible to implement network isolation on Linux using the
- # clone() syscall. However, I prefer to have the 'chroot' backend behave
- # the same on all platforms, and have separate Linux-specific backends to
- # do Linux-specific stuff.
-
assert network == 'undefined', \
"'%s' is an unsupported value for 'network' in the 'chroot' backend. " \
"Network sharing cannot be be configured in this backend." % network
diff --git a/sandboxlib/linux_user_chroot.py b/sandboxlib/linux_user_chroot.py
index 3397a1a..4b88e50 100644
--- a/sandboxlib/linux_user_chroot.py
+++ b/sandboxlib/linux_user_chroot.py
@@ -27,10 +27,6 @@ implementation here also uses 'unshare --mount', which can only be run as
linux-user-chroot to handle creating the new mount namespace and processing
any extra mounts would be a useful fix.
-Supported mounts settings: 'undefined', 'isolated'.
-
-Supported network settings: 'undefined', 'isolated'.
-
Much of this code is adapted from Morph, from the Baserock project, from code
written by Joe Burmeister, Richard Maw, Lars Wirzenius and others.
@@ -45,11 +41,16 @@ import tempfile
import sandboxlib
-def maximum_possible_isolation():
- return {
- 'mounts': 'isolated',
- 'network': 'isolated',
- }
+CAPABILITIES = {
+ 'network': ['isolated', 'undefined'],
+ 'mounts': ['isolated', 'undefined'],
+ 'writable_paths': ['all', 'any'],
+}
+
+
+def degrade_config_for_capabilities(in_config, warn=True):
+ # This backend has the most features, right now!
+ return in_config
def tmpfs_for_user():
@@ -116,13 +117,9 @@ def process_mount_config(mounts, extra_mounts):
# linux-user-chroot always calls clone(CLONE_NEWNS) which creates a new
# mount namespace. It also ensures that all mount points inside the sandbox
# are private, by calling mount("/", MS_PRIVATE | MS_REC). So 'isolated' is
- # the only option.
- supported_values = ['undefined', 'isolated']
+ # the only option for 'mounts'.
- assert mounts in supported_values, \
- "'%s' is an unsupported value for 'mounts' in the " \
- "'linux-user-chroot' backend. Supported values: %s" \
- % (mounts, ', '.join(supported_values))
+ sandboxlib.utils.check_parameter('mounts', mounts, CAPABILITIES['mounts'])
# This is only used if there are tmpfs mounts, but it's simpler to
# create it unconditionally.
@@ -150,12 +147,7 @@ def process_network_config(network):
# blocked'? Or does it mean 'working, with /etc/resolv.conf correctly set
# up'? So that's not handled yet.
- supported_values = ['undefined', 'isolated']
-
- assert network in supported_values, \
- "'%s' is an unsupported value for 'network' in the " \
- "'linux-user-chroot' backend. Supported values: %s" \
- % (network, ', '.join(supported_values))
+ sandboxlib.utils.check_parameter('network', network, CAPABILITIES['network'])
if network == 'isolated':
# This is all we need to do for network isolation
diff --git a/sandboxlib/utils.py b/sandboxlib/utils.py
index af5fe3e..b3ec867 100644
--- a/sandboxlib/utils.py
+++ b/sandboxlib/utils.py
@@ -22,6 +22,14 @@ import sys
import sandboxlib
+def check_parameter(name, value, supported_values):
+ assert value in supported_values, \
+ "'%(value)s' is an unsupported value for '%(name)s' in this " \
+ "backend. Supported values: %(supported_values)s".format(
+ name=name, value=value,
+ supported_values=', '.join(supported_values))
+
+
def find_program(program_name):
search_path = os.environ.get('PATH')
diff --git a/tests/test_all.py b/tests/test_all.py
index 38370f9..eed2f79 100644
--- a/tests/test_all.py
+++ b/tests/test_all.py
@@ -36,6 +36,21 @@ def sandboxlib_executor(request):
return executor
+def test_no_output(sandboxlib_executor):
+ '''Test ignoring of stderr/stdout.
+
+ We could use run_sandbox_with_redirection() and not get the 'err' and 'out'
+ paramemter at all, but we may as well test that they are indeed None.
+
+ '''
+ exit, out, err = sandboxlib_executor.run_sandbox(
+ ['echo', 'xyzzy'], stdout=None, stderr=None)
+
+ assert exit == 0
+ assert out is None
+ assert err is None
+
+
def test_stdout(sandboxlib_executor):
exit, out, err = sandboxlib_executor.run_sandbox(['echo', 'xyzzy'])
@@ -87,7 +102,6 @@ class TestMounts(object):
assert exit == 0
-
class TestWriteablePaths(object):
@pytest.fixture()
def writable_paths_test_sandbox(self, tmpdir,
@@ -182,3 +196,21 @@ class TestWriteablePaths(object):
assert out.decode('unicode-escape') == \
"Wrote data to /data/1/canary."
assert exit == 0
+
+
+def test_executor_for_platform():
+ '''Simple test of backend autodetection.'''
+ executor = sandboxlib.executor_for_platform()
+ test_stdout(executor)
+
+
+def test_degrade_config_for_capabilities(sandboxlib_executor):
+ '''Simple test of adjusting configuration for a given backend.'''
+ in_config = {
+ 'mounts': 'isolated',
+ 'network': 'isolated',
+ 'filesystem_writable_paths': ['/tmp']
+ }
+
+ out_config = sandboxlib_executor.degrade_config_for_capabilities(
+ in_config, warn=True)