from __future__ import absolute_import import copy try: from unittest import mock except ImportError: import mock import unittest import six.moves.configparser # inject builder data from tests.test_builder.loadkojid import kojid import __main__ __main__.BuildRoot = kojid.BuildRoot import koji import koji.util import runroot if six.PY2: CONFIG_PARSER = 'six.moves.configparser.SafeConfigParser' else: CONFIG_PARSER = 'six.moves.configparser.ConfigParser' CONFIG1 = { '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', }} CONFIG2 = { 'paths': { 'default_mounts': '/mnt/archive,/mnt/workdir', 'safe_roots': '/mnt/workdir/tmp', 'path_subs': '\n' '/mnt/archive/prehistory/,/mnt/prehistoric_disk/archive/prehistory\n' '/mnt/archve/workdir,/mnt/workdir\n', }, 'path0': { 'mountpoint': '/mnt/archive', 'path': 'archive.org:/vol/archive', 'fstype': 'nfs', 'options': 'ro,hard,intr,nosuid,nodev,noatime,tcp', }, 'path1': { 'mountpoint': '/mnt/workdir', 'path': 'archive.org:/vol/workdir', 'fstype': 'nfs', 'options': 'ro,hard,intr,nosuid,nodev,noatime,tcp', }, 'path2': { 'mountpoint': '/mnt/prehistoric_disk', 'path': 'archive.org:/vol/prehistoric_disk', 'fstype': 'nfs', 'options': 'ro,hard,intr,nosuid,nodev,noatime,tcp', }} class FakeConfigParser(object): def __init__(self, config=None): if config is None: self.CONFIG = copy.deepcopy(CONFIG1) else: self.CONFIG = copy.deepcopy(config) def read(self, path, encoding=None): return def sections(self): return list(self.CONFIG.keys()) 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 six.moves.configparser.NoOptionError(section, key) class TestRunrootConfig(unittest.TestCase): @mock.patch(CONFIG_PARSER) def test_bad_config_paths0(self, config_parser): cp = FakeConfigParser() del cp.CONFIG['path0']['mountpoint'] config_parser.return_value = cp session = mock.MagicMock() options = mock.MagicMock() options.workdir = '/tmp/nonexistentdirectory' with self.assertRaises(koji.GenericError) as cm: runroot.RunRootTask(123, 'runroot', {}, session, options) self.assertEqual(cm.exception.args[0], "bad config: missing options in path0 section") @mock.patch(CONFIG_PARSER) def test_bad_config_absolute_path(self, config_parser): cp = FakeConfigParser() cp.CONFIG['paths']['default_mounts'] = '' config_parser.return_value = cp session = mock.MagicMock() options = mock.MagicMock() options.workdir = '/tmp/nonexistentdirectory' with self.assertRaises(koji.GenericError) as cm: runroot.RunRootTask(123, 'runroot', {}, session, options) self.assertEqual(cm.exception.args[0], "bad config: all paths (default_mounts, safe_roots, path_subs) " "needs to be absolute: ") @mock.patch(CONFIG_PARSER) def test_valid_config(self, config_parser): config_parser.return_value = FakeConfigParser() session = mock.MagicMock() options = mock.MagicMock() options.workdir = '/tmp/nonexistentdirectory' runroot.RunRootTask(123, 'runroot', {}, session, options) @mock.patch(CONFIG_PARSER) def test_valid_config_alt(self, config_parser): config_parser.return_value = FakeConfigParser(CONFIG2) session = mock.MagicMock() options = mock.MagicMock() options.workdir = '/tmp/nonexistentdirectory' runroot.RunRootTask(123, 'runroot', {}, session, options) @mock.patch(CONFIG_PARSER) def test_pathnum_gaps(self, config_parser): session = mock.MagicMock() options = mock.MagicMock() options.workdir = '/tmp/nonexistentdirectory' config = CONFIG2.copy() config_parser.return_value = FakeConfigParser(config) task1 = runroot.RunRootTask(123, 'runroot', {}, session, options) # adjust the path numbers (but preserving order) and do it again config = CONFIG2.copy() config['path99'] = config['path1'] config['path999'] = config['path2'] del config['path1'] del config['path2'] config_parser.return_value = FakeConfigParser(config) task2 = runroot.RunRootTask(123, 'runroot', {}, session, options) # resulting processed config should be the same self.assertEqual(task1.config, task2.config) paths = list([CONFIG2[k] for k in ('path0', 'path1', 'path2')]) self.assertEqual(task2.config['paths'], paths) @mock.patch(CONFIG_PARSER) def test_bad_path_sub(self, config_parser): session = mock.MagicMock() options = mock.MagicMock() options.workdir = '/tmp/nonexistentdirectory' config = copy.deepcopy(CONFIG2) config['paths']['path_subs'] += 'incorrect:format' config_parser.return_value = FakeConfigParser(config) with self.assertRaises(koji.GenericError): runroot.RunRootTask(123, 'runroot', {}, session, options) class TestMounts(unittest.TestCase): @mock.patch(CONFIG_PARSER) def setUp(self, config_parser): config_parser.return_value = FakeConfigParser() self.session = mock.MagicMock() options = mock.MagicMock() options.workdir = '/tmp/nonexistentdirectory' self.t = runroot.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('koji._open_text_file') @mock.patch('os.path.isdir') @mock.patch('runroot.log_output') def test_do_mounts(self, log_output, is_dir, open_mock): 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.args[0], "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.args[0], '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.args[0], "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.args[0], "bad config: background mount not allowed") def test_do_extra_mounts(self): self.t.do_mounts = mock.MagicMock() self.t._get_path_params = mock.MagicMock() self.t._get_path_params.return_value = 'path_params' # no mounts self.t.do_extra_mounts('rootdir', []) self.t.do_mounts.assert_called_once_with('rootdir', []) # safe mount self.t.do_mounts.reset_mock() self.t.do_extra_mounts('rootdir', ['/mnt/workdir/tmp/xyz']) self.t.do_mounts.assert_called_once_with('rootdir', ['path_params']) # unsafe mount self.t.do_mounts.reset_mock() with self.assertRaises(koji.GenericError): self.t.do_extra_mounts('rootdir', ['unsafe']) self.t.do_mounts.assert_not_called() # hackish mount self.t.do_mounts.reset_mock() with self.assertRaises(koji.GenericError): self.t.do_extra_mounts('rootdir', ['/mnt/workdir/tmp/../xyz']) self.t.do_mounts.assert_not_called() @mock.patch('koji._open_text_file') @mock.patch('runroot.scan_mounts') @mock.patch('os.unlink') @mock.patch('subprocess.Popen') @mock.patch('os.path.exists') def test_undo_mounts(self, path_exists, popen, os_unlink, scan_mounts, m_open): self.t.logger = mock.MagicMock() scan_mounts.return_value = ['mount_1', 'mount_2'] # correct popen.return_value.wait.return_value = 0 path_exists.return_value = True m_open.return_value.__enter__.return_value.readlines.return_value = ['mountpoint'] self.t.undo_mounts('rootdir') self.t.logger.assert_has_calls([ mock.call.debug('Unmounting runroot mounts'), mock.call.info("Unmounting (runroot): ['mountpoint', 'mount_2', 'mount_1']"), ]) os_unlink.assert_called_once_with('rootdir/tmp/runroot_mounts') # fail os_unlink.reset_mock() popen.return_value.wait.return_value = 1 popen.return_value.stdout.read.return_value = '' popen.return_value.stderr.read.return_value = 'error' path_exists.return_value = True with self.assertRaises(koji.GenericError) as cm: self.t.undo_mounts('rootdir') self.assertEqual(cm.exception.args[0], 'Unable to unmount: mountpoint: error, mount_2: error, mount_1: error') os_unlink.assert_not_called() class TestHandler(unittest.TestCase): @mock.patch(CONFIG_PARSER) def setUp(self, config_parser): self.session = mock.MagicMock() self.br = mock.MagicMock() self.br.mock.return_value = 0 self.br.id = 678 self.br.rootdir.return_value = '/rootdir' runroot.BuildRoot = mock.MagicMock() runroot.BuildRoot.return_value = self.br options = mock.MagicMock() options.workdir = '/tmp/nonexistentdirectory' # options.topurl = 'http://topurl' options.topurls = None self.t = runroot.RunRootTask(123, 'runroot', {}, self.session, options) self.t.config['default_mounts'] = ['default_mount'] self.t.config['internal_dev_setup'] = None self.t.do_mounts = mock.MagicMock() self.t.do_extra_mounts = mock.MagicMock() self.t.undo_mounts = mock.MagicMock() self.t.uploadFile = mock.MagicMock() self.t._get_path_params = mock.MagicMock() self.t._get_path_params.side_effect = lambda x: x def tearDown(self): runroot.BuildRoot = kojid.BuildRoot @mock.patch('os.unlink') @mock.patch('platform.uname') def test_handler_simple(self, platform_uname, os_unlink): platform_uname.return_value = ('system', 'node', 'release', 'version', 'machine', 'arch') self.session.getBuildConfig.return_value = { 'id': 456, 'name': 'tag_name', 'arches': 'noarch x86_64', 'extra': {}, } self.session.repoInfo.return_value = { 'id': 1, 'create_event': 123, 'state': koji.REPO_STATES['READY'], 'tag_id': 456, 'tag_name': 'tag_name', } self.session.getRepo.return_value = { 'id': 1, } self.session.host.getHost.return_value = {'arches': 'x86_64'} self.t.handler('tag_name', 'noarch', 'command', weight=10.0, repo_id=1, packages=['rpm_a', 'rpm_b'], new_chroot=True, mounts=['/mnt/a'], skip_setarch=True, upload_logs=['log_1', 'log_2']) # calls self.session.host.setTaskWeight.assert_called_once_with(self.t.id, 10.0) self.session.host.getHost.assert_called_once_with() self.session.getBuildConfig.assert_called_once_with('tag_name') self.session.repoInfo.assert_called_once_with(1, strict=True) self.session.host.subtask.assert_not_called() runroot.BuildRoot.assert_called_once_with( self.session, self.t.options, 'tag_name', 'x86_64', self.t.id, repo_id=1, setup_dns=True, internal_dev_setup=None) self.session.host.setBuildRootState.assert_called_once_with(678, 'BUILDING') self.br.mock.assert_has_calls([ mock.call(['--install', 'rpm_a', 'rpm_b']), mock.call(['--new-chroot', '--arch', 'arch', '--chroot', '--', '/bin/sh', '-c', '{ command; } < /dev/null 2>&1 | /usr/bin/tee /builddir/runroot.log; ' 'exit ${PIPESTATUS[0]}']), ]) self.session.host.updateBuildRootList.assert_called_once_with( 678, self.br.getPackageList()) self.t.do_mounts.assert_called_once_with('/rootdir', ['default_mount']) self.t.do_extra_mounts.assert_called_once_with('/rootdir', ['/mnt/a']) self.t.uploadFile.assert_has_calls([ mock.call('/rootdir/builddir/runroot.log'), mock.call('/rootdir/log_1'), mock.call('/rootdir/log_2'), ])