diff --git a/Makefile b/Makefile index 9a0bd346..bc8767f4 100644 --- a/Makefile +++ b/Makefile @@ -66,7 +66,7 @@ git-clean: test: coverage erase - PYTHONPATH=hub/.:plugins/hub/. nosetests --with-coverage --cover-package . + PYTHONPATH=hub/.:plugins/hub/.:plugins/builder/. nosetests --with-coverage --cover-package . coverage html @echo Coverage report in htmlcov/index.html diff --git a/plugins/builder/runroot.py b/plugins/builder/runroot.py index 6b90ee27..fe23bd05 100644 --- a/plugins/builder/runroot.py +++ b/plugins/builder/runroot.py @@ -246,7 +246,7 @@ class RunRootTask(tasks.BaseTaskHandler): self.logger.info('New runroot') self.logger.info("Runroot mounts: %s" % mounts) fn = '%s/tmp/runroot_mounts' % rootdir - fslog = file(fn, 'a') + fslog = open(fn, 'a') logfile = "%s/do_mounts.log" % self.workdir uploadpath = self.getUploadDir() error = None @@ -264,9 +264,6 @@ class RunRootTask(tasks.BaseTaskHandler): error = koji.GenericError("No such directory or mount: %s" % dev) break type = 'none' - if path is None: - #shorthand for "same path" - path = dev if 'bg' in opts: error = koji.GenericError("bad config: background mount not allowed") break @@ -294,8 +291,8 @@ class RunRootTask(tasks.BaseTaskHandler): mounts = {} fn = '%s/tmp/runroot_mounts' % rootdir if os.path.exists(fn): - fslog = file(fn, 'r') - for line in fslog: + fslog = open(fn, 'r') + for line in fslog.readlines(): mounts.setdefault(line.strip(), 1) fslog.close() #also, check /proc/mounts just in case diff --git a/tests/test_plugins/test_runroot_builder.py b/tests/test_plugins/test_runroot_builder.py new file mode 100644 index 00000000..e841d98f --- /dev/null +++ b/tests/test_plugins/test_runroot_builder.py @@ -0,0 +1,202 @@ +import unittest +import mock +import ConfigParser + +# inject builder data +from tests.test_builder.loadkojid import kojid +import __main__ +__main__.BuildRoot = kojid.BuildRoot + +import koji +from runroot import RunRootTask + +class FakeConfigParser(object): + def __init__(self): + self.CONFIG = { + 'paths': { + 'default_mounts': '/mnt/archive,/mnt/workdir', + 'safe_roots': '/mnt/workdir/tmp', + 'path_subs': + '/mnt/archive/prehistory/,/mnt/prehistoric_disk/archive/prehistory', + }, + 'path0': { + 'mountpoint': '/mnt/archive', + 'path': 'archive.org:/vol/archive', + 'fstype': 'nfs', + 'options': 'ro,hard,intr,nosuid,nodev,noatime,tcp', + }, + } + + def read(self, path): + return + + def has_option(self, section, key): + return section in self.CONFIG and key in self.CONFIG[section] + + def has_section(self, section): + return section in self.CONFIG + + def get(self, section, key): + try: + return self.CONFIG[section][key] + except KeyError: + raise ConfigParser.NoOptionError(section, key) + + +class TestRunrootConfig(unittest.TestCase): + @mock.patch('ConfigParser.SafeConfigParser') + def test_bad_config_paths0(self, safe_config_parser): + cp = FakeConfigParser() + del cp.CONFIG['path0']['mountpoint'] + safe_config_parser.return_value = cp + session = mock.MagicMock() + options = mock.MagicMock() + options.workdir = '/tmp/nonexistentdirectory' + with self.assertRaises(koji.GenericError) as cm: + RunRootTask(123, 'runroot', {}, session, options) + self.assertEqual(cm.exception.message, + "bad config: missing options in path0 section") + + @mock.patch('ConfigParser.SafeConfigParser') + def test_bad_config_absolute_path(self, safe_config_parser): + cp = FakeConfigParser() + cp.CONFIG['paths']['default_mounts'] = '' + safe_config_parser.return_value = cp + session = mock.MagicMock() + options = mock.MagicMock() + options.workdir = '/tmp/nonexistentdirectory' + with self.assertRaises(koji.GenericError) as cm: + RunRootTask(123, 'runroot', {}, session, options) + self.assertEqual(cm.exception.message, + "bad config: all paths (default_mounts, safe_roots, path_subs) needs to be absolute: ") + + @mock.patch('ConfigParser.SafeConfigParser') + def test_valid_config(self, safe_config_parser): + safe_config_parser.return_value = FakeConfigParser() + session = mock.MagicMock() + options = mock.MagicMock() + options.workdir = '/tmp/nonexistentdirectory' + RunRootTask(123, 'runroot', {}, session, options) + +class TestMounts(unittest.TestCase): + @mock.patch('ConfigParser.SafeConfigParser') + def setUp(self, safe_config_parser): + safe_config_parser.return_value = FakeConfigParser() + self.session = mock.MagicMock() + options = mock.MagicMock() + options.workdir = '/tmp/nonexistentdirectory' + self.t = RunRootTask(123, 'runroot', {}, self.session, options) + + def test_get_path_params(self): + # non-existent item + with self.assertRaises(koji.GenericError): + self.t._get_path_params('nonexistent_dir') + + # valid item + self.assertEqual(self.t._get_path_params('/mnt/archive', 'rw'), + ('archive.org:/vol/archive/', '/mnt/archive', 'nfs', 'rw,hard,intr,nosuid,nodev,noatime,tcp')) + + @mock.patch('os.path.isdir') + @mock.patch('runroot.open') + @mock.patch('runroot.log_output') + def test_do_mounts(self, log_output, file_mock, is_dir): + log_output.return_value = 0 # successful mount + + # no mounts, don't do anything + self.t.logger = mock.MagicMock() + self.t.do_mounts('rootdir', []) + self.t.logger.assert_not_called() + + # mountpoint has no absolute_path + with self.assertRaises(koji.GenericError) as cm: + self.t.do_mounts('rootdir', [('nfs:nfs', 'relative_path', 'nfs', '')]) + self.assertEqual(cm.exception.message, + "invalid mount point: relative_path") + + # cover missing opts + self.t.do_mounts('rootdir', [('nfs:nfs', '/mnt/archive', 'nfs', None)]) + + # standard + log_output.reset_mock() + mounts = [self.t._get_path_params('/mnt/archive')] + self.t.do_mounts('rootdir', mounts) + log_output.assert_called_once_with(self.session, 'mount', + ['mount', '-t', 'nfs', '-o', 'ro,hard,intr,nosuid,nodev,noatime,tcp', + 'archive.org:/vol/archive/', 'rootdir/mnt/archive'], + '/tmp/nonexistentdirectory/tasks/123/123/do_mounts.log', + 'tasks/123/123', append=True, logerror=True) + + # mount command failed + log_output.reset_mock() + log_output.return_value = 1 + #self.t.undo_mounts = mock.MagicMock() + mounts = [self.t._get_path_params('/mnt/archive')] + with self.assertRaises(koji.GenericError) as cm: + self.t.do_mounts('rootdir', mounts) + self.assertEqual(cm.exception.message, + 'Unable to mount rootdir/mnt/archive: mount -t nfs -o' + ' ro,hard,intr,nosuid,nodev,noatime,tcp archive.org:/vol/archive/' + ' rootdir/mnt/archive was killed by signal 1') + + # bind ok + log_output.return_value = 0 + log_output.reset_mock() + mount = list(self.t._get_path_params('/mnt/archive')) + mount[3] += ',bind' + is_dir.return_value = True + self.t.do_mounts('rootdir', [mount]) + log_output.assert_called_once_with(self.session, 'mount', + ['mount', '-t', 'none', '-o', 'ro,hard,intr,nosuid,nodev,noatime,tcp,bind', + 'archive.org:/vol/archive/', 'rootdir/mnt/archive'], + '/tmp/nonexistentdirectory/tasks/123/123/do_mounts.log', + 'tasks/123/123', append=True, logerror=True) + + # bind - target doesn't exist + mount = list(self.t._get_path_params('/mnt/archive')) + mount[3] += ',bind' + is_dir.return_value = False + with self.assertRaises(koji.GenericError) as cm: + self.t.do_mounts('rootdir', [mount]) + self.assertEqual(cm.exception.message, + "No such directory or mount: archive.org:/vol/archive/") + + # bg option forbidden + log_output.reset_mock() + mount = list(self.t._get_path_params('/mnt/archive')) + mount[3] += ',bg' + is_dir.return_value = False + with self.assertRaises(koji.GenericError) as cm: + self.t.do_mounts('rootdir', [mount]) + self.assertEqual(cm.exception.message, + "bad config: background mount not allowed") + + @mock.patch('os.unlink') + @mock.patch('commands.getstatusoutput') + @mock.patch('os.path.exists') + def test_undo_mounts(self, path_exists, getstatusoutput, os_unlink): + self.t.logger = mock.MagicMock() + + # correct + getstatusoutput.return_value = (0, 'ok') + path_exists.return_value = True + with mock.patch('runroot.open', mock.mock_open(read_data = 'mountpoint')): + self.t.undo_mounts('rootdir') + self.t.logger.assert_has_calls([ + mock.call.debug('Unmounting runroot mounts'), + mock.call.info("Unmounting (runroot): ['mountpoint']"), + ]) + os_unlink.assert_called_once_with('rootdir/tmp/runroot_mounts') + + # fail + os_unlink.reset_mock() + getstatusoutput.return_value = (1, 'error') + path_exists.return_value = True + with mock.patch('runroot.open', mock.mock_open(read_data = 'mountpoint')): + with self.assertRaises(koji.GenericError) as cm: + self.t.undo_mounts('rootdir') + self.assertEqual(cm.exception.message, 'Unable to unmount: mountpoint: error') + os_unlink.assert_not_called() + +class TestHandler(unittest.TestCase): + # TODO + pass diff --git a/tests/test_plugins/test_runroot_hub.py b/tests/test_plugins/test_runroot_hub.py index dd132890..79b5696b 100644 --- a/tests/test_plugins/test_runroot_hub.py +++ b/tests/test_plugins/test_runroot_hub.py @@ -1,6 +1,7 @@ import unittest import mock +import koji import runroot_hub @@ -21,3 +22,125 @@ class TestRunrootHub(unittest.TestCase): arch='x86_64', channel='runroot', ) + + @mock.patch('kojihub.get_tag') + @mock.patch('runroot_hub.context') + def test_noarch_wrong_tag(self, context, get_tag): + context.session.assertPerm = mock.MagicMock() + get_tag.return_value = {'name': 'some_tag', 'arches': ''} + with self.assertRaises(koji.GenericError): + runroot_hub.runroot( + tagInfo='some_tag', + arch='noarch', + command='ls', + ) + get_tag.assert_called_once_with('some_tag') + + @mock.patch('kojihub.make_task') + @mock.patch('kojihub.get_all_arches') + @mock.patch('kojihub.get_tag') + @mock.patch('runroot_hub.context') + def test_noarch_good_tag(self, context, get_tag, get_all_arches, make_task): + context.session.assertPerm = mock.MagicMock() + context.handlers = mock.MagicMock() + context.handlers.call = mock.MagicMock() + context.handlers.call.side_effect = [ + {'id': 2, 'name': 'runroot'}, # getChannel + [ # listHosts + { + 'arches': 'i386 x86_64', + 'capacity': 20.0, + 'comment': '', + 'description': '', + 'enabled': True, + 'id': 1, + 'name': 'builder.example.com', + 'ready': True, + 'task_load': 0.0, + 'user_id': 1 + } + ] + ] + get_tag.return_value = { + 'arches': 's390 x86_64', + 'extra': {}, + 'id': 123456, + 'locked': False, + 'maven_include_all': False, + 'maven_support': False, + 'name': 'some_tag', + 'perm': None, + 'perm_id': None + } + get_all_arches.return_value = ['s390', 's390x', 'x86_64'] + runroot_hub.runroot( + tagInfo='some_tag', + arch='noarch', + command='ls', + ) + + # check results + get_tag.assert_called_once_with('some_tag') + context.handlers.call.assert_has_calls([ + mock.call('getChannel', 'runroot', strict=True), + mock.call('listHosts', channelID=2, enabled=True), + ]) + make_task.assert_called_once_with( + 'runroot', + ('some_tag', 'noarch', 'ls'), + priority=15, + arch='x86_64', + channel='runroot', + ) + + @mock.patch('kojihub.make_task') + @mock.patch('kojihub.get_all_arches') + @mock.patch('kojihub.get_tag') + @mock.patch('runroot_hub.context') + def test_noarch_good_tag_missing_arch(self, context, get_tag, get_all_arches, make_task): + context.session.assertPerm = mock.MagicMock() + context.handlers = mock.MagicMock() + context.handlers.call = mock.MagicMock() + context.handlers.call.side_effect = [ + {'id': 2, 'name': 'runroot'}, # getChannel + [ # listHosts + { + 'arches': 'i386 x86_64', + 'capacity': 20.0, + 'comment': '', + 'description': '', + 'enabled': True, + 'id': 1, + 'name': 'builder.example.com', + 'ready': True, + 'task_load': 0.0, + 'user_id': 1 + } + ] + ] + get_tag.return_value = { + 'arches': 's390', + 'extra': {}, + 'id': 123456, + 'locked': False, + 'maven_include_all': False, + 'maven_support': False, + 'name': 'some_tag', + 'perm': None, + 'perm_id': None + } + get_all_arches.return_value = ['s390x'] + with self.assertRaises(koji.GenericError): + runroot_hub.runroot( + tagInfo='some_tag', + arch='noarch', + command='ls', + ) + + # check results + get_tag.assert_called_once_with('some_tag') + context.handlers.call.assert_has_calls([ + mock.call('getChannel', 'runroot', strict=True), + mock.call('listHosts', channelID=2, enabled=True), + ]) + make_task.assert_not_called()