diff options
author | Tiago Natel de Moura <t.nateldemoura@f5.com> | 2020-05-28 14:59:52 +0100 |
---|---|---|
committer | Tiago Natel de Moura <t.nateldemoura@f5.com> | 2020-05-28 14:59:52 +0100 |
commit | 08b765ae4289f399bb3642d327ccf402efca3537 (patch) | |
tree | 972afb5db99730305f22c71a05bd319a9ad526a7 /test | |
parent | e2b53e16c60ba1e3bbbe59172c184e97f889326b (diff) | |
download | unit-08b765ae4289f399bb3642d327ccf402efca3537.tar.gz unit-08b765ae4289f399bb3642d327ccf402efca3537.tar.bz2 |
Tests: Added rootfs tests.
Diffstat (limited to 'test')
-rw-r--r-- | test/go/ns_inspect/app.go | 21 | ||||
-rw-r--r-- | test/python/ns_inspect/wsgi.py | 31 | ||||
-rw-r--r-- | test/test_go_isolation.py | 46 | ||||
-rw-r--r-- | test/test_go_isolation_rootfs.py | 34 | ||||
-rw-r--r-- | test/test_java_isolation_rootfs.py | 85 | ||||
-rw-r--r-- | test/test_php_isolation.py | 57 | ||||
-rw-r--r-- | test/test_python_isolation.py | 79 | ||||
-rw-r--r-- | test/test_python_isolation_chroot.py | 57 | ||||
-rw-r--r-- | test/test_ruby_isolation.py | 71 | ||||
-rw-r--r-- | test/unit/applications/lang/go.py | 75 | ||||
-rw-r--r-- | test/unit/applications/lang/python.py | 19 | ||||
-rw-r--r-- | test/unit/main.py | 3 |
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): |