Tests: isolation check moved to the pytest_sessionstart().

This change eliminates the need for some classes
to run Unit one more time before running tests.
This commit is contained in:
Andrei Zeliankou
2020-12-09 16:15:50 +00:00
parent 783cdc2a3d
commit 4c846ae693
10 changed files with 197 additions and 259 deletions

View File

@@ -17,6 +17,7 @@ import pytest
from unit.check.go import check_go from unit.check.go import check_go
from unit.check.node import check_node from unit.check.node import check_node
from unit.check.tls import check_openssl from unit.check.tls import check_openssl
from unit.check.isolation import check_isolation
from unit.option import option from unit.option import option
from unit.utils import public_dir from unit.utils import public_dir
from unit.utils import waitforfiles from unit.utils import waitforfiles
@@ -123,6 +124,7 @@ def pytest_sessionstart(session):
option.available = {'modules': {}, 'features': {}} option.available = {'modules': {}, 'features': {}}
unit = unit_run() unit = unit_run()
option.temp_dir = unit['temp_dir']
# read unit.log # read unit.log
@@ -161,6 +163,8 @@ def pytest_sessionstart(session):
k: v for k, v in option.available['modules'].items() if v is not None k: v for k, v in option.available['modules'].items() if v is not None
} }
check_isolation()
unit_stop() unit_stop()
_check_alerts() _check_alerts()

View File

@@ -5,31 +5,13 @@ import shutil
import pytest import pytest
from conftest import unit_run
from conftest import unit_stop
from unit.applications.lang.go import TestApplicationGo from unit.applications.lang.go import TestApplicationGo
from unit.feature.isolation import TestFeatureIsolation
from unit.option import option from unit.option import option
from unit.utils import getns
class TestGoIsolation(TestApplicationGo): class TestGoIsolation(TestApplicationGo):
prerequisites = {'modules': {'go': 'any'}, 'features': ['isolation']} prerequisites = {'modules': {'go': 'any'}, 'features': ['isolation']}
isolation = TestFeatureIsolation()
@classmethod
def setup_class(cls, complete_check=True):
check = super().setup_class(complete_check=False)
unit = unit_run()
option.temp_dir = unit['temp_dir']
TestFeatureIsolation().check(option.available, unit['temp_dir'])
assert unit_stop() is None
shutil.rmtree(unit['temp_dir'])
return check if not complete_check else check()
def unpriv_creds(self): def unpriv_creds(self):
nobody_uid = pwd.getpwnam('nobody').pw_uid nobody_uid = pwd.getpwnam('nobody').pw_uid
@@ -219,8 +201,8 @@ class TestGoIsolation(TestApplicationGo):
== option.available['features']['isolation'][ns] == option.available['features']['isolation'][ns]
), ('%s match' % ns) ), ('%s match' % ns)
assert obj['NS']['MNT'] != self.isolation.getns('mnt'), 'mnt set' assert obj['NS']['MNT'] != getns('mnt'), 'mnt set'
assert obj['NS']['USER'] != self.isolation.getns('user'), 'user set' assert obj['NS']['USER'] != getns('user'), 'user set'
def test_isolation_pid(self, is_su): def test_isolation_pid(self, is_su):
if not self.isolation_key('pid'): if not self.isolation_key('pid'):

View File

@@ -2,30 +2,13 @@ import shutil
import pytest import pytest
from conftest import unit_run
from conftest import unit_stop
from unit.applications.lang.php import TestApplicationPHP from unit.applications.lang.php import TestApplicationPHP
from unit.feature.isolation import TestFeatureIsolation
from unit.option import option from unit.option import option
class TestPHPIsolation(TestApplicationPHP): class TestPHPIsolation(TestApplicationPHP):
prerequisites = {'modules': {'php': 'any'}, 'features': ['isolation']} prerequisites = {'modules': {'php': 'any'}, 'features': ['isolation']}
@classmethod
def setup_class(cls, complete_check=True):
check = super().setup_class(complete_check=False)
unit = unit_run()
option.temp_dir = unit['temp_dir']
TestFeatureIsolation().check(option.available, unit['temp_dir'])
assert unit_stop() is None
shutil.rmtree(unit['temp_dir'])
return check if not complete_check else check()
def test_php_isolation_rootfs(self, is_su, temp_dir): def test_php_isolation_rootfs(self, is_su, temp_dir):
isolation_features = option.available['features']['isolation'].keys() isolation_features = option.available['features']['isolation'].keys()

View File

@@ -2,30 +2,13 @@ import shutil
import pytest import pytest
from conftest import unit_run
from conftest import unit_stop
from unit.applications.lang.python import TestApplicationPython from unit.applications.lang.python import TestApplicationPython
from unit.feature.isolation import TestFeatureIsolation
from unit.option import option from unit.option import option
class TestPythonIsolation(TestApplicationPython): class TestPythonIsolation(TestApplicationPython):
prerequisites = {'modules': {'python': 'any'}, 'features': ['isolation']} prerequisites = {'modules': {'python': 'any'}, 'features': ['isolation']}
@classmethod
def setup_class(cls, complete_check=True):
check = super().setup_class(complete_check=False)
unit = unit_run()
option.temp_dir = unit['temp_dir']
TestFeatureIsolation().check(option.available, unit['temp_dir'])
assert unit_stop() is None
shutil.rmtree(unit['temp_dir'])
return check if not complete_check else check()
def test_python_isolation_rootfs(self, is_su, temp_dir): def test_python_isolation_rootfs(self, is_su, temp_dir):
isolation_features = option.available['features']['isolation'].keys() isolation_features = option.available['features']['isolation'].keys()

View File

@@ -1,7 +1,6 @@
import pytest import pytest
from unit.applications.lang.python import TestApplicationPython from unit.applications.lang.python import TestApplicationPython
from unit.feature.isolation import TestFeatureIsolation
class TestPythonIsolation(TestApplicationPython): class TestPythonIsolation(TestApplicationPython):

View File

@@ -3,30 +3,13 @@ import shutil
import os import os
import pytest import pytest
from conftest import unit_run
from conftest import unit_stop
from unit.applications.lang.ruby import TestApplicationRuby from unit.applications.lang.ruby import TestApplicationRuby
from unit.feature.isolation import TestFeatureIsolation
from unit.option import option from unit.option import option
class TestRubyIsolation(TestApplicationRuby): class TestRubyIsolation(TestApplicationRuby):
prerequisites = {'modules': {'ruby': 'any'}, 'features': ['isolation']} prerequisites = {'modules': {'ruby': 'any'}, 'features': ['isolation']}
@classmethod
def setup_class(cls, complete_check=True):
check = super().setup_class(complete_check=False)
unit = unit_run()
option.temp_dir = unit['temp_dir']
TestFeatureIsolation().check(option.available, unit['temp_dir'])
assert unit_stop() is None
shutil.rmtree(unit['temp_dir'])
return check if not complete_check else check()
def test_ruby_isolation_rootfs(self, is_su): def test_ruby_isolation_rootfs(self, is_su):
isolation_features = option.available['features']['isolation'].keys() isolation_features = option.available['features']['isolation'].keys()

View File

@@ -0,0 +1,158 @@
import json
import os
from unit.applications.lang.go import TestApplicationGo
from unit.applications.lang.java import TestApplicationJava
from unit.applications.lang.node import TestApplicationNode
from unit.applications.proto import TestApplicationProto
from unit.http import TestHTTP
from unit.option import option
from unit.utils import getns
allns = ['pid', 'mnt', 'ipc', 'uts', 'cgroup', 'net']
http = TestHTTP()
def check_isolation():
test_conf = {"namespaces": {"credential": True}}
available = option.available
conf = ''
if 'go' in available['modules']:
TestApplicationGo().prepare_env('empty', 'app')
conf = {
"listeners": {"*:7080": {"pass": "applications/empty"}},
"applications": {
"empty": {
"type": "external",
"processes": {"spare": 0},
"working_directory": option.test_dir + "/go/empty",
"executable": option.temp_dir + "/go/app",
"isolation": {"namespaces": {"credential": True}},
},
},
}
elif 'python' in available['modules']:
conf = {
"listeners": {"*:7080": {"pass": "applications/empty"}},
"applications": {
"empty": {
"type": "python",
"processes": {"spare": 0},
"path": option.test_dir + "/python/empty",
"working_directory": option.test_dir + "/python/empty",
"module": "wsgi",
"isolation": {"namespaces": {"credential": True}},
}
},
}
elif 'php' in available['modules']:
conf = {
"listeners": {"*:7080": {"pass": "applications/phpinfo"}},
"applications": {
"phpinfo": {
"type": "php",
"processes": {"spare": 0},
"root": option.test_dir + "/php/phpinfo",
"working_directory": option.test_dir + "/php/phpinfo",
"index": "index.php",
"isolation": {"namespaces": {"credential": True}},
}
},
}
elif 'ruby' in available['modules']:
conf = {
"listeners": {"*:7080": {"pass": "applications/empty"}},
"applications": {
"empty": {
"type": "ruby",
"processes": {"spare": 0},
"working_directory": option.test_dir + "/ruby/empty",
"script": option.test_dir + "/ruby/empty/config.ru",
"isolation": {"namespaces": {"credential": True}},
}
},
}
elif 'java' in available['modules']:
TestApplicationJava().prepare_env('empty')
conf = {
"listeners": {"*:7080": {"pass": "applications/empty"}},
"applications": {
"empty": {
"unit_jars": option.current_dir + "/build",
"type": "java",
"processes": {"spare": 0},
"working_directory": option.test_dir + "/java/empty/",
"webapp": option.temp_dir + "/java",
"isolation": {"namespaces": {"credential": True}},
}
},
}
elif 'node' in available['modules']:
TestApplicationNode().prepare_env('basic')
conf = {
"listeners": {"*:7080": {"pass": "applications/basic"}},
"applications": {
"basic": {
"type": "external",
"processes": {"spare": 0},
"working_directory": option.temp_dir + "/node",
"executable": "app.js",
"isolation": {"namespaces": {"credential": True}},
}
},
}
elif 'perl' in available['modules']:
conf = {
"listeners": {"*:7080": {"pass": "applications/body_empty"}},
"applications": {
"body_empty": {
"type": "perl",
"processes": {"spare": 0},
"working_directory": option.test_dir
+ "/perl/body_empty",
"script": option.test_dir + "/perl/body_empty/psgi.pl",
"isolation": {"namespaces": {"credential": True}},
}
},
}
else:
return
resp = http.put(
url='/config',
sock_type='unix',
addr=option.temp_dir + '/control.unit.sock',
body=json.dumps(conf),
)
if 'success' not in resp:
return
userns = getns('user')
if not userns:
return
available['features']['isolation'] = {'user': userns}
unp_clone_path = '/proc/sys/kernel/unprivileged_userns_clone'
if os.path.exists(unp_clone_path):
with open(unp_clone_path, 'r') as f:
if str(f.read()).rstrip() == '1':
available['features']['isolation'][
'unprivileged_userns_clone'
] = True
for ns in allns:
ns_value = getns(ns)
if ns_value:
available['features']['isolation'][ns] = ns_value

View File

@@ -1,160 +0,0 @@
import os
from unit.applications.lang.go import TestApplicationGo
from unit.applications.lang.java import TestApplicationJava
from unit.applications.lang.node import TestApplicationNode
from unit.applications.proto import TestApplicationProto
from unit.option import option
class TestFeatureIsolation(TestApplicationProto):
allns = ['pid', 'mnt', 'ipc', 'uts', 'cgroup', 'net']
def check(self, available, temp_dir):
test_conf = {"namespaces": {"credential": True}}
conf = ''
if 'go' in available['modules']:
TestApplicationGo().prepare_env('empty', 'app')
conf = {
"listeners": {"*:7080": {"pass": "applications/empty"}},
"applications": {
"empty": {
"type": "external",
"processes": {"spare": 0},
"working_directory": option.test_dir + "/go/empty",
"executable": option.temp_dir + "/go/app",
"isolation": {"namespaces": {"credential": True}},
},
},
}
elif 'python' in available['modules']:
conf = {
"listeners": {"*:7080": {"pass": "applications/empty"}},
"applications": {
"empty": {
"type": "python",
"processes": {"spare": 0},
"path": option.test_dir + "/python/empty",
"working_directory": option.test_dir + "/python/empty",
"module": "wsgi",
"isolation": {"namespaces": {"credential": True}},
}
},
}
elif 'php' in available['modules']:
conf = {
"listeners": {"*:7080": {"pass": "applications/phpinfo"}},
"applications": {
"phpinfo": {
"type": "php",
"processes": {"spare": 0},
"root": option.test_dir + "/php/phpinfo",
"working_directory": option.test_dir + "/php/phpinfo",
"index": "index.php",
"isolation": {"namespaces": {"credential": True}},
}
},
}
elif 'ruby' in available['modules']:
conf = {
"listeners": {"*:7080": {"pass": "applications/empty"}},
"applications": {
"empty": {
"type": "ruby",
"processes": {"spare": 0},
"working_directory": option.test_dir + "/ruby/empty",
"script": option.test_dir + "/ruby/empty/config.ru",
"isolation": {"namespaces": {"credential": True}},
}
},
}
elif 'java' in available['modules']:
TestApplicationJava().prepare_env('empty')
conf = {
"listeners": {"*:7080": {"pass": "applications/empty"}},
"applications": {
"empty": {
"unit_jars": option.current_dir + "/build",
"type": "java",
"processes": {"spare": 0},
"working_directory": option.test_dir + "/java/empty/",
"webapp": option.temp_dir + "/java",
"isolation": {"namespaces": {"credential": True}},
}
},
}
elif 'node' in available['modules']:
TestApplicationNode().prepare_env('basic')
conf = {
"listeners": {"*:7080": {"pass": "applications/basic"}},
"applications": {
"basic": {
"type": "external",
"processes": {"spare": 0},
"working_directory": option.temp_dir + "/node",
"executable": "app.js",
"isolation": {"namespaces": {"credential": True}},
}
},
}
elif 'perl' in available['modules']:
conf = {
"listeners": {"*:7080": {"pass": "applications/body_empty"}},
"applications": {
"body_empty": {
"type": "perl",
"processes": {"spare": 0},
"working_directory": option.test_dir
+ "/perl/body_empty",
"script": option.test_dir + "/perl/body_empty/psgi.pl",
"isolation": {"namespaces": {"credential": True}},
}
},
}
else:
return
if 'success' not in self.conf(conf):
return
userns = self.getns('user')
if not userns:
return
available['features']['isolation'] = {'user': userns}
unp_clone_path = '/proc/sys/kernel/unprivileged_userns_clone'
if os.path.exists(unp_clone_path):
with open(unp_clone_path, 'r') as f:
if str(f.read()).rstrip() == '1':
available['features']['isolation'][
'unprivileged_userns_clone'
] = True
for ns in self.allns:
ns_value = self.getns(ns)
if ns_value:
available['features']['isolation'][ns] = ns_value
def getns(self, nstype):
# read namespace id from symlink file:
# it points to: '<nstype>:[<ns id>]'
# # eg.: 'pid:[4026531836]'
nspath = '/proc/self/ns/' + nstype
data = None
if os.path.exists(nspath):
data = int(os.readlink(nspath)[len(nstype) + 2 : -1])
return data

View File

@@ -4,8 +4,7 @@ from unit.option import option
class TestUnit(): class TestUnit():
@classmethod @classmethod
def setup_class(cls, complete_check=True): def setup_class(cls):
def check():
missed = [] missed = []
# check modules # check modules
@@ -35,8 +34,3 @@ class TestUnit():
if missed: if missed:
pytest.skip(', '.join(missed) + ' feature(s) not supported') pytest.skip(', '.join(missed) + ' feature(s) not supported')
if complete_check:
check()
else:
return check

View File

@@ -48,3 +48,15 @@ def waitforsocket(port):
pytest.fail('Can\'t connect to the 127.0.0.1:' + port) pytest.fail('Can\'t connect to the 127.0.0.1:' + port)
def getns(nstype):
# read namespace id from symlink file:
# it points to: '<nstype>:[<ns id>]'
# # eg.: 'pid:[4026531836]'
nspath = '/proc/self/ns/' + nstype
data = None
if os.path.exists(nspath):
data = int(os.readlink(nspath)[len(nstype) + 2 : -1])
return data