diff --git a/salt/modules/chroot.py b/salt/modules/chroot.py index 6e4705b67ee..17b5890d8c7 100644 --- a/salt/modules/chroot.py +++ b/salt/modules/chroot.py @@ -50,16 +50,17 @@ def __virtual__(): return (False, 'Module chroot requires the command chroot') -def exist(name): +def exist(root): ''' Return True if the chroot environment is present. ''' - dev = os.path.join(name, 'dev') - proc = os.path.join(name, 'proc') - return all(os.path.isdir(i) for i in (name, dev, proc)) + dev = os.path.join(root, 'dev') + proc = os.path.join(root, 'proc') + sys = os.path.join(root, 'sys') + return all(os.path.isdir(i) for i in (root, dev, proc, sys)) -def create(name): +def create(root): ''' Create a basic chroot environment. @@ -67,7 +68,7 @@ def create(name): install the minimal required binaries, including Python if chroot.call is called. - name + root Path to the chroot environment CLI Example: @@ -77,26 +78,28 @@ def create(name): salt myminion chroot.create /chroot ''' - if not exist(name): - dev = os.path.join(name, 'dev') - proc = os.path.join(name, 'proc') + if not exist(root): + dev = os.path.join(root, 'dev') + proc = os.path.join(root, 'proc') + sys = os.path.join(root, 'sys') try: os.makedirs(dev, mode=0o755) os.makedirs(proc, mode=0o555) + os.makedirs(sys, mode=0o555) except OSError as e: log.error('Error when trying to create chroot directories: %s', e) return False return True -def call(name, function, *args, **kwargs): +def call(root, function, *args, **kwargs): ''' Executes a Salt function inside a chroot environment. The chroot does not need to have Salt installed, but Python is required. - name + root Path to the chroot environment function @@ -107,18 +110,19 @@ def call(name, function, *args, **kwargs): .. code-block:: bash salt myminion chroot.call /chroot test.ping + salt myminion chroot.call /chroot ssh.set_auth_key user key=mykey ''' if not function: raise CommandExecutionError('Missing function parameter') - if not exist(name): + if not exist(root): raise CommandExecutionError('Chroot environment not found') # Create a temporary directory inside the chroot where we can # untar salt-thin - thin_dest_path = tempfile.mkdtemp(dir=name) + thin_dest_path = tempfile.mkdtemp(dir=root) thin_path = __utils__['thin.gen_thin']( __opts__['cachedir'], extra_mods=__salt__['config.option']('thin_extra_mods', ''), @@ -130,7 +134,7 @@ def call(name, function, *args, **kwargs): return {'result': False, 'comment': stdout} chroot_path = os.path.join(os.path.sep, - os.path.relpath(thin_dest_path, name)) + os.path.relpath(thin_dest_path, root)) try: safe_kwargs = clean_kwargs(**kwargs) salt_argv = [ @@ -144,8 +148,8 @@ def call(name, function, *args, **kwargs): '-l', 'quiet', '--', function - ] + list(args) + ['{}={}'.format(k, v) for (k, v) in safe_kwargs] - ret = __salt__['cmd.run_chroot'](name, [str(x) for x in salt_argv]) + ] + list(args) + ['{}={}'.format(k, v) for (k, v) in safe_kwargs.items()] + ret = __salt__['cmd.run_chroot'](root, [str(x) for x in salt_argv]) if ret['retcode'] != EX_OK: raise CommandExecutionError(ret['stderr']) diff --git a/salt/modules/cmdmod.py b/salt/modules/cmdmod.py index d0819f2f790..b279d00a11a 100644 --- a/salt/modules/cmdmod.py +++ b/salt/modules/cmdmod.py @@ -3064,13 +3064,19 @@ def run_chroot(root, if isinstance(cmd, (list, tuple)): cmd = ' '.join([six.text_type(i) for i in cmd]) - cmd = 'chroot {0} {1} -c {2}'.format(root, sh_, _cmd_quote(cmd)) + + # If runas and group are provided, we expect that the user lives + # inside the chroot, not outside. + if runas: + userspec = '--userspec {}:{}'.format(runas, group if group else '') + else: + userspec = '' + + cmd = 'chroot {} {} {} -c {}'.format(userspec, root, sh_, _cmd_quote(cmd)) run_func = __context__.pop('cmd.run_chroot.func', run_all) ret = run_func(cmd, - runas=runas, - group=group, cwd=cwd, stdin=stdin, shell=shell, diff --git a/salt/modules/freezer.py b/salt/modules/freezer.py index 786dfe45152..85adbfeb825 100644 --- a/salt/modules/freezer.py +++ b/salt/modules/freezer.py @@ -151,7 +151,7 @@ def freeze(name=None, force=False, **kwargs): states_path = _states_path() try: - os.makedirs(states_path) + os.makedirs(states_path, exist_ok=True) except OSError as e: msg = 'Error when trying to create the freezer storage %s: %s' log.error(msg, states_path, e) @@ -163,13 +163,13 @@ def freeze(name=None, force=False, **kwargs): safe_kwargs = clean_kwargs(**kwargs) pkgs = __salt__['pkg.list_pkgs'](**safe_kwargs) repos = __salt__['pkg.list_repos'](**safe_kwargs) - for name, content in zip(_paths(name), (pkgs, repos)): - with fopen(name, 'w') as fp: + for fname, content in zip(_paths(name), (pkgs, repos)): + with fopen(fname, 'w') as fp: json.dump(content, fp) return True -def restore(name=None, **kwargs): +def restore(name=None, clean=False, **kwargs): ''' Make sure that the system contains the packages and repos from a frozen state. @@ -190,6 +190,9 @@ def restore(name=None, **kwargs): name Name of the frozen state. Optional. + clean + In True remove the frozen information YAML from the cache + CLI Example: .. code-block:: bash @@ -203,8 +206,8 @@ def restore(name=None, **kwargs): frozen_pkgs = {} frozen_repos = {} - for name, content in zip(_paths(name), (frozen_pkgs, frozen_repos)): - with fopen(name) as fp: + for fname, content in zip(_paths(name), (frozen_pkgs, frozen_repos)): + with fopen(fname) as fp: content.update(json.load(fp)) # The ordering of removing or adding packages and repos can be @@ -291,4 +294,9 @@ def restore(name=None, **kwargs): log.error(msg, repo, e) res['comment'].append(msg % (repo, e)) + # Clean the cached YAML files + if clean and not res['comment']: + for fname in _paths(name): + os.remove(fname) + return res diff --git a/salt/modules/zypperpkg.py b/salt/modules/zypperpkg.py index 6bc7211f591..f71d6aac9e4 100644 --- a/salt/modules/zypperpkg.py +++ b/salt/modules/zypperpkg.py @@ -449,8 +449,14 @@ def _clean_cache(): ''' Clean cached results ''' + keys = [] for cache_name in ['pkg.list_pkgs', 'pkg.list_provides']: - __context__.pop(cache_name, None) + for contextkey in __context__: + if contextkey.startswith(cache_name): + keys.append(contextkey) + + for key in keys: + __context__.pop(key, None) def list_upgrades(refresh=True, root=None, **kwargs): @@ -809,9 +815,10 @@ def list_pkgs(versions_as_list=False, root=None, includes=None, **kwargs): includes = includes if includes else [] - contextkey = 'pkg.list_pkgs' + # Results can be different if a different root or a different + # inclusion types are passed + contextkey = 'pkg.list_pkgs_{}_{}'.format(root, includes) - # TODO(aplanas): this cached value depends on the parameters if contextkey not in __context__: ret = {} cmd = ['rpm'] diff --git a/tests/unit/modules/test_chroot.py b/tests/unit/modules/test_chroot.py index 7181dd7e509..0e65a266068 100644 --- a/tests/unit/modules/test_chroot.py +++ b/tests/unit/modules/test_chroot.py @@ -63,10 +63,10 @@ def test_exist(self, isdir): ''' Test if the chroot environment exist. ''' - isdir.side_effect = (True, True, True) + isdir.side_effect = (True, True, True, True) self.assertTrue(chroot.exist('/chroot')) - isdir.side_effect = (True, True, False) + isdir.side_effect = (True, True, True, False) self.assertFalse(chroot.exist('/chroot')) @patch('os.makedirs') @@ -182,3 +182,35 @@ def test_call_success(self, mkdtemp, exist): salt_mock['archive.tar'].assert_called_once() salt_mock['cmd.run_chroot'].assert_called_once() utils_mock['files.rm_rf'].assert_called_once() + + @patch('salt.modules.chroot.exist') + @patch('tempfile.mkdtemp') + def test_call_success_parameters(self, mkdtemp, exist): + ''' + Test execution of Salt functions in chroot with parameters. + ''' + # Success test + exist.return_value = True + mkdtemp.return_value = '/chroot/tmp01' + utils_mock = { + 'thin.gen_thin': MagicMock(return_value='/salt-thin.tgz'), + 'files.rm_rf': MagicMock(), + 'json.find_json': MagicMock(return_value={'return': 'result'}) + } + salt_mock = { + 'archive.tar': MagicMock(return_value=''), + 'config.option': MagicMock(), + 'cmd.run_chroot': MagicMock(return_value={ + 'retcode': 0, + 'stdout': '', + }), + } + with patch.dict(chroot.__utils__, utils_mock), \ + patch.dict(chroot.__salt__, salt_mock): + self.assertEqual(chroot.call('/chroot', 'ssh.set_auth_key', + user='user', key='key'), 'result') + utils_mock['thin.gen_thin'].assert_called_once() + salt_mock['config.option'].assert_called() + salt_mock['archive.tar'].assert_called_once() + salt_mock['cmd.run_chroot'].assert_called_once() + utils_mock['files.rm_rf'].assert_called_once() diff --git a/tests/unit/modules/test_cmdmod.py b/tests/unit/modules/test_cmdmod.py index a20afaca0f6..6f3964f7aa7 100644 --- a/tests/unit/modules/test_cmdmod.py +++ b/tests/unit/modules/test_cmdmod.py @@ -312,6 +312,22 @@ def test_run_cwd_doesnt_exist_issue_7154(self): else: raise RuntimeError + @skipIf(salt.utils.platform.is_windows(), 'Do not run on Windows') + @skipIf(salt.utils.platform.is_darwin(), 'Do not run on MacOS') + def test_run_cwd_in_combination_with_runas(self): + ''' + cmd.run executes command in the cwd directory + when the runas parameter is specified + ''' + cmd = 'pwd' + cwd = '/tmp' + runas = os.getlogin() + + with patch.dict(cmdmod.__grains__, {'os': 'Darwin', + 'os_family': 'Solaris'}): + stdout = cmdmod._run(cmd, cwd=cwd, runas=runas).get('stdout') + self.assertEqual(stdout, cwd) + def test_run_all_binary_replace(self): ''' Test for failed decoding of binary data, for instance when doing @@ -401,3 +417,37 @@ def test_run_all_output_encoding(self): ret = cmdmod.run_all('some command', output_encoding='latin1') self.assertEqual(ret['stdout'], stdout) + + def test_run_chroot_runas(self): + ''' + Test run_chroot when a runas parameter is provided + ''' + with patch.dict(cmdmod.__salt__, {'mount.mount': MagicMock(), + 'mount.umount': MagicMock()}): + with patch('salt.modules.cmdmod.run_all') as run_all_mock: + cmdmod.run_chroot('/mnt', 'ls', runas='foobar') + run_all_mock.assert_called_with( + 'chroot --userspec foobar: /mnt /bin/sh -c ls', + bg=False, + clean_env=False, + cwd=None, + env=None, + ignore_retcode=False, + log_callback=None, + output_encoding=None, + output_loglevel='quiet', + pillar=None, + pillarenv=None, + python_shell=True, + reset_system_locale=True, + rstrip=True, + saltenv='base', + shell='/bin/bash', + stdin=None, + success_retcodes=None, + success_stderr=None, + success_stdout=None, + template=None, + timeout=None, + umask=None, + use_vt=False) diff --git a/tests/unit/modules/test_freezer.py b/tests/unit/modules/test_freezer.py index f6cf2f374f0..70d315c17a3 100644 --- a/tests/unit/modules/test_freezer.py +++ b/tests/unit/modules/test_freezer.py @@ -112,6 +112,30 @@ def test_freeze_fails_already_frozen(self, makedirs, status): self.assertRaises(CommandExecutionError, freezer.freeze) makedirs.assert_called_once() + @patch('salt.utils.json.dump') + @patch('salt.modules.freezer.fopen') + @patch('salt.modules.freezer.status') + @patch('os.makedirs') + def test_freeze_success_two_freeze(self, makedirs, status, fopen, dump): + ''' + Test to freeze a current installation + ''' + # Freeze the current new state + status.return_value = False + salt_mock = { + 'pkg.list_pkgs': MagicMock(return_value={}), + 'pkg.list_repos': MagicMock(return_value={}), + } + with patch.dict(freezer.__salt__, salt_mock): + self.assertTrue(freezer.freeze('one')) + self.assertTrue(freezer.freeze('two')) + + self.assertEqual(makedirs.call_count, 2) + self.assertEqual(salt_mock['pkg.list_pkgs'].call_count, 2) + self.assertEqual(salt_mock['pkg.list_repos'].call_count, 2) + fopen.assert_called() + dump.assert_called() + @patch('salt.utils.json.dump') @patch('salt.modules.freezer.fopen') @patch('salt.modules.freezer.status') @@ -132,7 +156,7 @@ def test_freeze_success_new_state(self, makedirs, status, fopen, dump): salt_mock['pkg.list_pkgs'].assert_called_once() salt_mock['pkg.list_repos'].assert_called_once() fopen.assert_called() - dump.asster_called() + dump.assert_called() @patch('salt.utils.json.dump') @patch('salt.modules.freezer.fopen') @@ -154,7 +178,7 @@ def test_freeze_success_force(self, makedirs, status, fopen, dump): salt_mock['pkg.list_pkgs'].assert_called_once() salt_mock['pkg.list_repos'].assert_called_once() fopen.assert_called() - dump.asster_called() + dump.assert_called() @patch('salt.modules.freezer.status') def test_restore_fails_missing_state(self, status): @@ -190,7 +214,7 @@ def test_restore_add_missing_repo(self, status, fopen, load): salt_mock['pkg.list_repos'].assert_called() salt_mock['pkg.mod_repo'].assert_called_once() fopen.assert_called() - load.asster_called() + load.assert_called() @patch('salt.utils.json.load') @patch('salt.modules.freezer.fopen') @@ -217,7 +241,7 @@ def test_restore_add_missing_package(self, status, fopen, load): salt_mock['pkg.list_repos'].assert_called() salt_mock['pkg.install'].assert_called_once() fopen.assert_called() - load.asster_called() + load.assert_called() @patch('salt.utils.json.load') @patch('salt.modules.freezer.fopen') @@ -244,7 +268,7 @@ def test_restore_remove_extra_package(self, status, fopen, load): salt_mock['pkg.list_repos'].assert_called() salt_mock['pkg.remove'].assert_called_once() fopen.assert_called() - load.asster_called() + load.assert_called() @patch('salt.utils.json.load') @patch('salt.modules.freezer.fopen') @@ -271,4 +295,30 @@ def test_restore_remove_extra_repo(self, status, fopen, load): salt_mock['pkg.list_repos'].assert_called() salt_mock['pkg.del_repo'].assert_called_once() fopen.assert_called() - load.asster_called() + load.assert_called() + + @patch('os.remove') + @patch('salt.utils.json.load') + @patch('salt.modules.freezer.fopen') + @patch('salt.modules.freezer.status') + def test_restore_clean_yml(self, status, fopen, load, remove): + ''' + Test to restore an old state + ''' + status.return_value = True + salt_mock = { + 'pkg.list_pkgs': MagicMock(return_value={}), + 'pkg.list_repos': MagicMock(return_value={}), + 'pkg.install': MagicMock(), + } + with patch.dict(freezer.__salt__, salt_mock): + self.assertEqual(freezer.restore(clean=True), { + 'pkgs': {'add': [], 'remove': []}, + 'repos': {'add': [], 'remove': []}, + 'comment': [], + }) + salt_mock['pkg.list_pkgs'].assert_called() + salt_mock['pkg.list_repos'].assert_called() + fopen.assert_called() + load.assert_called() + self.assertEqual(remove.call_count, 2) diff --git a/tests/unit/modules/test_zypperpkg.py b/tests/unit/modules/test_zypperpkg.py index 0a3053680f4..695d982ca6c 100644 --- a/tests/unit/modules/test_zypperpkg.py +++ b/tests/unit/modules/test_zypperpkg.py @@ -570,6 +570,7 @@ def _add_data(data, key, value): patch.dict(zypper.__salt__, {'pkg_resource.stringify': MagicMock()}): pkgs = zypper.list_pkgs(versions_as_list=True) self.assertFalse(pkgs.get('gpg-pubkey', False)) + self.assertTrue('pkg.list_pkgs_None_[]' in zypper.__context__) for pkg_name, pkg_version in { 'jakarta-commons-discovery': ['0.4-129.686'], 'yast2-ftp-server': ['3.1.8-8.1'], @@ -612,6 +613,7 @@ def _add_data(data, key, value): patch.dict(pkg_resource.__salt__, {'pkg.parse_arch_from_name': zypper.parse_arch_from_name}): pkgs = zypper.list_pkgs(attr=['epoch', 'release', 'arch', 'install_date_time_t']) self.assertFalse(pkgs.get('gpg-pubkey', False)) + self.assertTrue('pkg.list_pkgs_None_[]' in zypper.__context__) for pkg_name, pkg_attr in { 'jakarta-commons-discovery': [{ 'version': '0.4', @@ -1455,3 +1457,22 @@ def test_list_patterns(self, get_visible_patterns): 'summary': 'description b', }, } + + def test__clean_cache_empty(self): + '''Test that an empty cached can be cleaned''' + context = {} + with patch.dict(zypper.__context__, context): + zypper._clean_cache() + assert context == {} + + def test__clean_cache_filled(self): + '''Test that a filled cached can be cleaned''' + context = { + 'pkg.list_pkgs_/mnt_[]': None, + 'pkg.list_pkgs_/mnt_[patterns]': None, + 'pkg.list_provides': None, + 'pkg.other_data': None, + } + with patch.dict(zypper.__context__, context): + zypper._clean_cache() + self.assertEqual(zypper.__context__, {'pkg.other_data': None})