summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorTiago Natel de Moura <t.nateldemoura@f5.com>2020-05-28 14:59:52 +0100
committerTiago Natel de Moura <t.nateldemoura@f5.com>2020-05-28 14:59:52 +0100
commit08b765ae4289f399bb3642d327ccf402efca3537 (patch)
tree972afb5db99730305f22c71a05bd319a9ad526a7
parente2b53e16c60ba1e3bbbe59172c184e97f889326b (diff)
downloadunit-08b765ae4289f399bb3642d327ccf402efca3537.tar.gz
unit-08b765ae4289f399bb3642d327ccf402efca3537.tar.bz2
Tests: Added rootfs tests.
-rw-r--r--test/go/ns_inspect/app.go21
-rw-r--r--test/python/ns_inspect/wsgi.py31
-rw-r--r--test/test_go_isolation.py46
-rw-r--r--test/test_go_isolation_rootfs.py34
-rw-r--r--test/test_java_isolation_rootfs.py85
-rw-r--r--test/test_php_isolation.py57
-rw-r--r--test/test_python_isolation.py79
-rw-r--r--test/test_python_isolation_chroot.py57
-rw-r--r--test/test_ruby_isolation.py71
-rw-r--r--test/unit/applications/lang/go.py75
-rw-r--r--test/unit/applications/lang/python.py19
-rw-r--r--test/unit/main.py3
12 files changed, 543 insertions, 35 deletions
diff --git a/test/go/ns_inspect/app.go b/test/go/ns_inspect/app.go
index d9b561c9..4d19a796 100644
--- a/test/go/ns_inspect/app.go
+++ b/test/go/ns_inspect/app.go
@@ -21,10 +21,11 @@ type (
}
Output struct {
- PID int
- UID int
- GID int
- NS NS
+ PID int
+ UID int
+ GID int
+ NS NS
+ FileExists bool
}
)
@@ -64,6 +65,18 @@ func handler(w http.ResponseWriter, r *http.Request) {
CGROUP: getns("cgroup"),
},
}
+
+ err := r.ParseForm()
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ return
+ }
+
+ if fname := r.Form.Get("file"); fname != "" {
+ _, err = os.Stat(fname);
+ out.FileExists = err == nil
+ }
+
data, err := json.Marshal(out)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
diff --git a/test/python/ns_inspect/wsgi.py b/test/python/ns_inspect/wsgi.py
new file mode 100644
index 00000000..fa1222e4
--- /dev/null
+++ b/test/python/ns_inspect/wsgi.py
@@ -0,0 +1,31 @@
+import json
+import os
+
+try:
+ # Python 3
+ from urllib.parse import parse_qs
+except ImportError:
+ # Python 2
+ from urlparse import parse_qs
+
+
+def application(environ, start_response):
+ ret = {
+ 'FileExists': False,
+ }
+
+ d = parse_qs(environ['QUERY_STRING'])
+
+ ret['FileExists'] = os.path.exists(d.get('path')[0])
+
+ out = json.dumps(ret)
+
+ start_response(
+ '200',
+ [
+ ('Content-Type', 'application/json'),
+ ('Content-Length', str(len(out))),
+ ],
+ )
+
+ return out.encode('utf-8')
diff --git a/test/test_go_isolation.py b/test/test_go_isolation.py
index e6aade9b..61d39617 100644
--- a/test/test_go_isolation.py
+++ b/test/test_go_isolation.py
@@ -281,6 +281,52 @@ class TestGoIsolation(TestApplicationGo):
'%s match' % ns,
)
+ def test_go_isolation_rootfs_container(self):
+ if not self.isolation_key('unprivileged_userns_clone'):
+ print('unprivileged clone is not available')
+ raise unittest.SkipTest()
+
+ if not self.isolation_key('mnt'):
+ print('mnt namespace is not supported')
+ raise unittest.SkipTest()
+
+ isolation = {
+ 'namespaces': {'mount': True, 'credential': True},
+ 'rootfs': self.testdir,
+ }
+
+ self.load('ns_inspect', isolation=isolation)
+
+ obj = self.getjson(url='/?file=/go/app')['body']
+
+ self.assertEqual(obj['FileExists'], True, 'app relative to rootfs')
+
+ obj = self.getjson(url='/?file=/bin/sh')['body']
+ self.assertEqual(obj['FileExists'], False, 'file should not exists')
+
+ def test_go_isolation_rootfs_container_priv(self):
+ if not self.is_su:
+ print("requires root")
+ raise unittest.SkipTest()
+
+ if not self.isolation_key('mnt'):
+ print('mnt namespace is not supported')
+ raise unittest.SkipTest()
+
+ isolation = {
+ 'namespaces': {'mount': True},
+ 'rootfs': self.testdir,
+ }
+
+ self.load('ns_inspect', isolation=isolation)
+
+ obj = self.getjson(url='/?file=/go/app')['body']
+
+ self.assertEqual(obj['FileExists'], True, 'app relative to rootfs')
+
+ obj = self.getjson(url='/?file=/bin/sh')['body']
+ self.assertEqual(obj['FileExists'], False, 'file should not exists')
+
if __name__ == '__main__':
TestGoIsolation.main()
diff --git a/test/test_go_isolation_rootfs.py b/test/test_go_isolation_rootfs.py
new file mode 100644
index 00000000..0039ff87
--- /dev/null
+++ b/test/test_go_isolation_rootfs.py
@@ -0,0 +1,34 @@
+import os
+import unittest
+
+from unit.applications.lang.go import TestApplicationGo
+
+
+class TestGoIsolationRootfs(TestApplicationGo):
+ prerequisites = {'modules': {'go': 'all'}}
+
+ def test_go_isolation_rootfs_chroot(self):
+ if not self.is_su:
+ print("requires root")
+ raise unittest.SkipTest()
+
+ if os.uname().sysname == 'Darwin':
+ print('chroot tests not supported on OSX')
+ raise unittest.SkipTest()
+
+ isolation = {
+ 'rootfs': self.testdir,
+ }
+
+ self.load('ns_inspect', isolation=isolation)
+
+ obj = self.getjson(url='/?file=/go/app')['body']
+
+ self.assertEqual(obj['FileExists'], True, 'app relative to rootfs')
+
+ obj = self.getjson(url='/?file=/bin/sh')['body']
+ self.assertEqual(obj['FileExists'], False, 'file should not exists')
+
+
+if __name__ == '__main__':
+ TestGoIsolationRootfs.main()
diff --git a/test/test_java_isolation_rootfs.py b/test/test_java_isolation_rootfs.py
new file mode 100644
index 00000000..4d39bdc3
--- /dev/null
+++ b/test/test_java_isolation_rootfs.py
@@ -0,0 +1,85 @@
+import os
+import subprocess
+import unittest
+
+from unit.applications.lang.java import TestApplicationJava
+
+
+class TestJavaIsolationRootfs(TestApplicationJava):
+ prerequisites = {'modules': {'java': 'all'}}
+
+ def setUp(self):
+ if not self.is_su:
+ return
+
+ super().setUp()
+
+ os.makedirs(self.testdir + '/jars')
+ os.makedirs(self.testdir + '/tmp')
+ os.chmod(self.testdir + '/tmp', 0o777)
+
+ try:
+ process = subprocess.Popen(
+ [
+ "mount",
+ "--bind",
+ self.pardir + "/build",
+ self.testdir + "/jars",
+ ],
+ stderr=subprocess.STDOUT,
+ )
+
+ process.communicate()
+
+ except:
+ self.fail('Cann\'t run mount process.')
+
+ def tearDown(self):
+ if not self.is_su:
+ return
+
+ try:
+ process = subprocess.Popen(
+ ["umount", "--lazy", self.testdir + "/jars"],
+ stderr=subprocess.STDOUT,
+ )
+
+ process.communicate()
+
+ except:
+ self.fail('Cann\'t run mount process.')
+
+ # super teardown must happen after unmount to avoid deletion of /build
+ super().tearDown()
+
+ def test_java_isolation_rootfs_chroot_war(self):
+ if not self.is_su:
+ print('require root')
+ raise unittest.SkipTest()
+
+ isolation = {
+ 'rootfs': self.testdir,
+ }
+
+ self.load('empty_war', isolation=isolation)
+
+ self.assertIn(
+ 'success',
+ self.conf(
+ '"/"', '/config/applications/empty_war/working_directory',
+ ),
+ )
+
+ self.assertIn(
+ 'success', self.conf('"/jars"', 'applications/empty_war/unit_jars')
+ )
+ self.assertIn(
+ 'success',
+ self.conf('"/java/empty.war"', 'applications/empty_war/webapp'),
+ )
+
+ self.assertEqual(self.get()['status'], 200, 'war')
+
+
+if __name__ == '__main__':
+ TestJavaIsolationRootfs.main()
diff --git a/test/test_php_isolation.py b/test/test_php_isolation.py
new file mode 100644
index 00000000..1b70ef02
--- /dev/null
+++ b/test/test_php_isolation.py
@@ -0,0 +1,57 @@
+import unittest
+
+from unit.applications.lang.php import TestApplicationPHP
+from unit.feature.isolation import TestFeatureIsolation
+
+
+class TestPHPIsolation(TestApplicationPHP):
+ prerequisites = {'modules': {'php': 'any'}, 'features': ['isolation']}
+
+ isolation = TestFeatureIsolation()
+
+ @classmethod
+ def setUpClass(cls, complete_check=True):
+ unit = super().setUpClass(complete_check=False)
+
+ TestFeatureIsolation().check(cls.available, unit.testdir)
+
+ return unit if not complete_check else unit.complete()
+
+ def test_php_isolation_rootfs(self):
+ isolation_features = self.available['features']['isolation'].keys()
+
+ if 'mnt' not in isolation_features:
+ print('requires mnt ns')
+ raise unittest.SkipTest()
+
+ if not self.is_su:
+ if 'user' not in isolation_features:
+ print('requires unprivileged userns or root')
+ raise unittest.SkipTest()
+
+ if not 'unprivileged_userns_clone' in isolation_features:
+ print('requires unprivileged userns or root')
+ raise unittest.SkipTest()
+
+ isolation = {
+ 'namespaces': {'credential': not self.is_su, 'mount': True},
+ 'rootfs': self.current_dir,
+ }
+
+ self.load('phpinfo', isolation=isolation)
+
+ self.assertIn(
+ 'success', self.conf('"/php/phpinfo"', 'applications/phpinfo/root')
+ )
+ self.assertIn(
+ 'success',
+ self.conf(
+ '"/php/phpinfo"', 'applications/phpinfo/working_directory'
+ ),
+ )
+
+ self.assertEqual(self.get()['status'], 200, 'empty rootfs')
+
+
+if __name__ == '__main__':
+ TestPHPIsolation.main()
diff --git a/test/test_python_isolation.py b/test/test_python_isolation.py
new file mode 100644
index 00000000..1bed64ba
--- /dev/null
+++ b/test/test_python_isolation.py
@@ -0,0 +1,79 @@
+import unittest
+
+from unit.applications.lang.python import TestApplicationPython
+from unit.feature.isolation import TestFeatureIsolation
+
+
+class TestPythonIsolation(TestApplicationPython):
+ prerequisites = {'modules': {'python': 'any'}, 'features': ['isolation']}
+
+ isolation = TestFeatureIsolation()
+
+ @classmethod
+ def setUpClass(cls, complete_check=True):
+ unit = super().setUpClass(complete_check=False)
+
+ TestFeatureIsolation().check(cls.available, unit.testdir)
+
+ return unit if not complete_check else unit.complete()
+
+ def test_python_isolation_rootfs(self):
+ isolation_features = self.available['features']['isolation'].keys()
+
+ if 'mnt' not in isolation_features:
+ print('requires mnt ns')
+ raise unittest.SkipTest()
+
+ if not self.is_su:
+ if 'user' not in isolation_features:
+ print('requires unprivileged userns or root')
+ raise unittest.SkipTest()
+
+ if not 'unprivileged_userns_clone' in isolation_features:
+ print('requires unprivileged userns or root')
+ raise unittest.SkipTest()
+
+ isolation = {
+ 'namespaces': {'credential': not self.is_su, 'mount': True},
+ 'rootfs': self.testdir,
+ }
+
+ self.load('empty', isolation=isolation)
+
+ self.assertEqual(self.get()['status'], 200, 'python rootfs')
+
+ self.load('ns_inspect', isolation=isolation)
+
+ self.assertEqual(
+ self.getjson(url='/?path=' + self.testdir)['body']['FileExists'],
+ False,
+ 'testdir does not exists in rootfs',
+ )
+
+ self.assertEqual(
+ self.getjson(url='/?path=/proc/self')['body']['FileExists'],
+ False,
+ 'no /proc/self',
+ )
+
+ self.assertEqual(
+ self.getjson(url='/?path=/dev/pts')['body']['FileExists'],
+ False,
+ 'no /dev/pts',
+ )
+
+ self.assertEqual(
+ self.getjson(url='/?path=/sys/kernel')['body']['FileExists'],
+ False,
+ 'no /sys/kernel',
+ )
+
+ ret = self.getjson(url='/?path=/app/python/ns_inspect')
+
+ self.assertEqual(
+ ret['body']['FileExists'], True, 'application exists in rootfs',
+ )
+
+
+if __name__ == '__main__':
+ TestPythonIsolation.main()
diff --git a/test/test_python_isolation_chroot.py b/test/test_python_isolation_chroot.py
new file mode 100644
index 00000000..7761128e
--- /dev/null
+++ b/test/test_python_isolation_chroot.py
@@ -0,0 +1,57 @@
+import unittest
+
+from unit.applications.lang.python import TestApplicationPython
+from unit.feature.isolation import TestFeatureIsolation
+
+
+class TestPythonIsolation(TestApplicationPython):
+ prerequisites = {'modules': {'python': 'any'}}
+
+ def test_python_isolation_chroot(self):
+ if not self.is_su:
+ print('requires root')
+ raise unittest.SkipTest()
+
+ isolation = {
+ 'rootfs': self.testdir,
+ }
+
+ self.load('empty', isolation=isolation)
+
+ self.assertEqual(self.get()['status'], 200, 'python chroot')
+
+ self.load('ns_inspect', isolation=isolation)
+
+ self.assertEqual(
+ self.getjson(url='/?path=' + self.testdir)['body']['FileExists'],
+ False,
+ 'testdir does not exists in rootfs',
+ )
+
+ self.assertEqual(
+ self.getjson(url='/?path=/proc/self')['body']['FileExists'],
+ False,
+ 'no /proc/self',
+ )
+
+ self.assertEqual(
+ self.getjson(url='/?path=/dev/pts')['body']['FileExists'],
+ False,
+ 'no /dev/pts',
+ )
+
+ self.assertEqual(
+ self.getjson(url='/?path=/sys/kernel')['body']['FileExists'],
+ False,
+ 'no /sys/kernel',
+ )
+
+ ret = self.getjson(url='/?path=/app/python/ns_inspect')
+
+ self.assertEqual(
+ ret['body']['FileExists'], True, 'application exists in rootfs',
+ )
+
+
+if __name__ == '__main__':
+ TestPythonIsolation.main()
diff --git a/test/test_ruby_isolation.py b/test/test_ruby_isolation.py
new file mode 100644
index 00000000..9bac162e
--- /dev/null
+++ b/test/test_ruby_isolation.py
@@ -0,0 +1,71 @@
+import os
+import shutil
+import unittest
+
+from unit.applications.lang.ruby import TestApplicationRuby
+from unit.feature.isolation import TestFeatureIsolation
+
+
+class TestRubyIsolation(TestApplicationRuby):
+ prerequisites = {'modules': {'ruby': 'any'}, 'features': ['isolation']}
+
+ isolation = TestFeatureIsolation()
+
+ @classmethod
+ def setUpClass(cls, complete_check=True):
+ unit = super().setUpClass(complete_check=False)
+
+ TestFeatureIsolation().check(cls.available, unit.testdir)
+
+ return unit if not complete_check else unit.complete()
+
+ def test_ruby_isolation_rootfs(self):
+ isolation_features = self.available['features']['isolation'].keys()
+
+ if 'mnt' not in isolation_features:
+ print('requires mnt ns')
+ raise unittest.SkipTest()
+
+ if not self.is_su:
+ if 'user' not in isolation_features:
+ print('requires unprivileged userns or root')
+ raise unittest.SkipTest()
+
+ if not 'unprivileged_userns_clone' in isolation_features:
+ print('requires unprivileged userns or root')
+ raise unittest.SkipTest()
+
+ os.mkdir(self.testdir + '/ruby')
+
+ shutil.copytree(
+ self.current_dir + '/ruby/status_int',
+ self.testdir + '/ruby/status_int',
+ )
+ isolation = {
+ 'namespaces': {'credential': not self.is_su, 'mount': True},
+ 'rootfs': self.testdir,
+ }
+
+ self.load('status_int', isolation=isolation)
+
+ self.assertIn(
+ 'success',
+ self.conf(
+ '"/ruby/status_int/config.ru"',
+ 'applications/status_int/script',
+ ),
+ )
+
+ self.assertIn(
+ 'success',
+ self.conf(
+ '"/ruby/status_int"',
+ 'applications/status_int/working_directory',
+ ),
+ )
+
+ self.assertEqual(self.get()['status'], 200, 'status int')
+
+
+if __name__ == '__main__':
+ TestRubyIsolation.main()
diff --git a/test/unit/applications/lang/go.py b/test/unit/applications/lang/go.py
index 9070beb6..83bde4d8 100644
--- a/test/unit/applications/lang/go.py
+++ b/test/unit/applications/lang/go.py
@@ -19,26 +19,36 @@ class TestApplicationGo(TestApplicationProto):
return unit if not complete_check else unit.complete()
- def prepare_env(self, script, name):
+ def prepare_env(self, script, name, static=False):
if not os.path.exists(self.testdir + '/go'):
os.mkdir(self.testdir + '/go')
env = os.environ.copy()
env['GOPATH'] = self.pardir + '/build/go'
- try:
- process = subprocess.Popen(
- [
- 'go',
- 'build',
- '-o',
- self.testdir + '/go/' + name,
- self.current_dir + '/go/' + script + '/' + name + '.go',
- ],
- env=env,
- stderr=subprocess.STDOUT,
- )
+ if static:
+ args = [
+ 'go',
+ 'build',
+ '-tags',
+ 'netgo',
+ '-ldflags',
+ '-extldflags "-static"',
+ '-o',
+ self.testdir + '/go/' + name,
+ self.current_dir + '/go/' + script + '/' + name + '.go',
+ ]
+ else:
+ args = [
+ 'go',
+ 'build',
+ '-o',
+ self.testdir + '/go/' + name,
+ self.current_dir + '/go/' + script + '/' + name + '.go',
+ ]
+ try:
+ process = subprocess.Popen(args, env=env)
process.communicate()
except:
@@ -47,21 +57,28 @@ class TestApplicationGo(TestApplicationProto):
return process
def load(self, script, name='app', **kwargs):
- self.prepare_env(script, name)
-
- self._load_conf(
- {
- "listeners": {"*:7080": {"pass": "applications/" + script}},
- "applications": {
- script: {
- "type": "external",
- "processes": {"spare": 0},
- "working_directory": self.current_dir
- + "/go/"
- + script,
- "executable": self.testdir + "/go/" + name,
- }
+ static_build = False
+
+ wdir = self.current_dir + "/go/" + script
+ executable = self.testdir + "/go/" + name
+
+ if 'isolation' in kwargs and 'rootfs' in kwargs['isolation']:
+ wdir = "/go/"
+ executable = "/go/" + name
+ static_build = True
+
+ self.prepare_env(script, name, static=static_build)
+
+ conf = {
+ "listeners": {"*:7080": {"pass": "applications/" + script}},
+ "applications": {
+ script: {
+ "type": "external",
+ "processes": {"spare": 0},
+ "working_directory": wdir,
+ "executable": executable,
},
},
- **kwargs
- )
+ }
+
+ self._load_conf(conf, **kwargs)
diff --git a/test/unit/applications/lang/python.py b/test/unit/applications/lang/python.py
index fdda024a..31a04107 100644
--- a/test/unit/applications/lang/python.py
+++ b/test/unit/applications/lang/python.py
@@ -1,3 +1,6 @@
+import shutil
+import os
+
from unit.applications.proto import TestApplicationProto
@@ -8,7 +11,21 @@ class TestApplicationPython(TestApplicationProto):
if name is None:
name = script
- script_path = self.current_dir + '/python/' + script
+ if script[0] == '/':
+ script_path = script
+ else:
+ script_path = self.current_dir + '/python/' + script
+
+ if kwargs.get('isolation') and kwargs['isolation'].get('rootfs'):
+ rootfs = kwargs['isolation']['rootfs']
+
+ if not os.path.exists(rootfs + '/app/python/'):
+ os.makedirs(rootfs + '/app/python/')
+
+ if not os.path.exists(rootfs + '/app/python/' + name):
+ shutil.copytree(script_path, rootfs + '/app/python/' + name)
+
+ script_path = '/app/python/' + name
self._load_conf(
{
diff --git a/test/unit/main.py b/test/unit/main.py
index d415f58f..408cf31c 100644
--- a/test/unit/main.py
+++ b/test/unit/main.py
@@ -58,6 +58,7 @@ class TestUnit(unittest.TestCase):
if prereq_version == 'all':
for version in available_versions:
self.application_type = type + ' ' + version
+ self.application_version = version
super().run(result)
elif prereq_version == 'any':
self.application_type = type + ' ' + available_versions[0]
@@ -165,7 +166,7 @@ class TestUnit(unittest.TestCase):
self._run()
def _run(self):
- build_dir = self.pardir + '/build'
+ build_dir = os.path.join(self.pardir, 'build')
self.unitd = build_dir + '/unitd'
if not os.path.isfile(self.unitd):