diff --git a/conf/master b/conf/master index ce2e26872af..680019b076e 100644 --- a/conf/master +++ b/conf/master @@ -506,6 +506,12 @@ # Boolean to run command via sudo. #ssh_sudo: False +# Boolean to run ssh_pre_flight script defined in roster. By default +# the script will only run if the thin_dir does not exist on the targeted +# minion. This forces the script to run regardless of the thin dir existing +# or not. +#ssh_run_pre_flight: True + # Number of seconds to wait for a response when establishing an SSH connection. #ssh_timeout: 60 diff --git a/doc/ref/cli/salt-ssh.rst b/doc/ref/cli/salt-ssh.rst index 70454abae59..df76cfcc07d 100644 --- a/doc/ref/cli/salt-ssh.rst +++ b/doc/ref/cli/salt-ssh.rst @@ -105,6 +105,14 @@ Options Pass a JID to be used instead of generating one. +.. option:: --pre-flight + + Run the ssh_pre_flight script defined in the roster. + By default this script will only run if the thin dir + does not exist on the target minion. This option will + force the script to run regardless of the thin dir + existing or not. + Authentication Options ---------------------- diff --git a/doc/ref/configuration/master.rst b/doc/ref/configuration/master.rst index 64f94b1b430..c27676e1479 100644 --- a/doc/ref/configuration/master.rst +++ b/doc/ref/configuration/master.rst @@ -1341,6 +1341,15 @@ salt-ssh. groupA: minion1,minion2 groupB: minion1,minion3 +.. conf_master:: ssh_run_pre_flight + +Default: False + +Run the ssh_pre_flight script defined in the salt-ssh roster. Be default +the script will only run when the thin dir does not exist on the targeted +minion. This will force the script to run and not check if the thin dir +exists first. + .. conf_master:: thin_extra_mods ``thin_extra_mods`` diff --git a/doc/topics/releases/sodium.rst b/doc/topics/releases/sodium.rst index 035b26a54aa..cbbdafadeb6 100644 --- a/doc/topics/releases/sodium.rst +++ b/doc/topics/releases/sodium.rst @@ -43,4 +43,7 @@ run this script. ssh_pre_flight: /srv/salt/pre_flight.sh The `ssh_pre_flight` script will only run if the thin dir is not currently on the -minion. +minion. If you want to force the script to run you have the following options: + - Wipe the thin dir on the targeted minion using the -w arg. + - Set ssh_run_pre_flight to True in the config. + - Run salt-ssh with the --pre-flight arg. diff --git a/doc/topics/ssh/roster.rst b/doc/topics/ssh/roster.rst index b1e4dace6cd..61757f1a633 100644 --- a/doc/topics/ssh/roster.rst +++ b/doc/topics/ssh/roster.rst @@ -63,7 +63,9 @@ The information which can be stored in a roster ``target`` is the following: # octal (so for 0o077 in YAML you would do 0077, or 63) ssh_pre_flight: # Path to a script that will run before all other salt-ssh # commands. Will only run the first time when the thin dir - # does not exist. Added in Sodium Release. + # does not exist, unless --pre-flight is passed to salt-ssh + # command or ssh_run_pre_flight is set to true in the config + # Added in Sodium Release. .. _ssh_pre_flight: @@ -74,7 +76,11 @@ A Salt-SSH roster option `ssh_pre_flight` was added in the Sodium release. This you to run a script before Salt-SSH tries to run any commands. You can set this option in the roster for a specific minion or use the `roster_defaults` to set it for all minions. This script will only run if the thin dir is not currently on the minion. This means it will -only run on the first run of salt-ssh or if you have recently wiped out your thin dir. +only run on the first run of salt-ssh or if you have recently wiped out your thin dir. If +you want to intentionally run the script again you have a couple of options: + - Wipe out your thin dir by using the -w salt-ssh arg. + - Set ssh_run_pre_flight to True in the config + - Run salt-ssh with the --pre-flight arg. .. _roster_defaults: diff --git a/salt/client/ssh/__init__.py b/salt/client/ssh/__init__.py index ea1adca70ed..8de605ad9a0 100644 --- a/salt/client/ssh/__init__.py +++ b/salt/client/ssh/__init__.py @@ -991,6 +991,15 @@ class Single(object): return self.execute_script(script) + def check_thin_dir(self): + ''' + check if the thindir exists on the remote machine + ''' + stdout, stderr, retcode = self.shell.exec_cmd('test -d {0}'.format(self.thin_dir)) + if retcode != 0: + return False + return True + def deploy(self): """ Deploy salt-thin @@ -1026,8 +1035,8 @@ class Single(object): stdout = stderr = retcode = None if self.ssh_pre_flight: - if os.path.exists(self.thin_dir): - log.debug('{0} thin dir already exists. Not running ssh_pre_flight script'.format(self.thin_dir)) + if self.check_thin_dir() and not self.opts.get('ssh_run_pre_flight', False): + log.info('{0} thin dir already exists. Not running ssh_pre_flight script'.format(self.thin_dir)) elif not os.path.exists(self.ssh_pre_flight): log.error('The ssh_pre_flight script {0} does not exist'.format(self.ssh_pre_flight)) else: @@ -1035,7 +1044,7 @@ class Single(object): if stderr: log.error('Error running ssh_pre_flight script {0}'.format(self.ssh_pre_file)) return stdout, stderr, retcode - log.debug('Successfully ran the ssh_pre_flight script: {0}'.format(self.ssh_pre_file)) + log.info('Successfully ran the ssh_pre_flight script: {0}'.format(self.ssh_pre_file)) if self.opts.get("raw_shell", False): cmd_str = " ".join([self._escape_arg(arg) for arg in self.argv]) diff --git a/salt/config/__init__.py b/salt/config/__init__.py index 6d39c1c954a..ad08c14a771 100644 --- a/salt/config/__init__.py +++ b/salt/config/__init__.py @@ -786,6 +786,7 @@ VALID_OPTS = immutabletypes.freeze( "ssh_log_file": six.string_types, "ssh_config_file": six.string_types, "ssh_merge_pillar": bool, + "ssh_run_pre_flight": bool, "cluster_mode": bool, "sqlite_queue_dir": six.string_types, "queue_dirs": list, diff --git a/salt/utils/parsers.py b/salt/utils/parsers.py index 2dd23596424..c7fca044ab2 100644 --- a/salt/utils/parsers.py +++ b/salt/utils/parsers.py @@ -3268,6 +3268,14 @@ class SaltSSHOptionParser( help="Pass a JID to be used instead of generating one.", ) + self.add_option( + '--pre-flight', + default=False, + action='store_true', + dest='ssh_run_pre_flight', + help='Run the defined ssh_pre_flight script in the roster' + ) + ssh_group = optparse.OptionGroup( self, "SSH Options", "Parameters for the SSH client." ) diff --git a/tests/integration/ssh/test_deploy.py b/tests/integration/ssh/test_deploy.py index e79f9841915..fa991f26b0f 100644 --- a/tests/integration/ssh/test_deploy.py +++ b/tests/integration/ssh/test_deploy.py @@ -10,10 +10,6 @@ import shutil # Import salt testing libs from tests.support.case import SSHCase -from tests.support.runtests import RUNTIME_VARS - -# Import salt libs -import salt.utils.yaml class SSHTest(SSHCase): @@ -38,26 +34,6 @@ class SSHTest(SSHCase): os.path.exists(os.path.join(thin_dir, "salt-call")) os.path.exists(os.path.join(thin_dir, "running_data")) - def test_ssh_pre_flight(self): - ''' - test ssh when ssh_pre_flight is set - ensure the script runs successfully - ''' - roster = os.path.join(RUNTIME_VARS.TMP, 'pre_flight_roster') - - data = {'ssh_pre_flight': os.path.join(RUNTIME_VARS.TMP, 'ssh_pre_flight.sh')} - self.custom_roster(roster, data) - - test_script = os.path.join(RUNTIME_VARS.TMP, - 'test-pre-flight-script-worked.txt') - - with salt.utils.files.fopen(data['ssh_pre_flight'], 'w') as fp_: - fp_.write('touch {0}'.format(test_script)) - - ret = self.run_function('test.ping', roster_file=roster) - - assert os.path.exists(test_script) - def tearDown(self): """ make sure to clean up any old ssh directories diff --git a/tests/integration/ssh/test_pre_flight.py b/tests/integration/ssh/test_pre_flight.py new file mode 100644 index 00000000000..98738e1ca18 --- /dev/null +++ b/tests/integration/ssh/test_pre_flight.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +''' +Test for ssh_pre_flight roster option +''' +# Import Python libs +from __future__ import absolute_import, print_function, unicode_literals +import os +import shutil + +# Import salt testing libs +from tests.support.case import SSHCase +from tests.support.runtests import RUNTIME_VARS + +# import salt libs +import salt.utils.files + + +class SSHPreFlightTest(SSHCase): + ''' + Test ssh_pre_flight roster option + ''' + def setUp(self): + self.roster = os.path.join(RUNTIME_VARS.TMP, 'pre_flight_roster') + self.data = {'ssh_pre_flight': os.path.join(RUNTIME_VARS.TMP, 'ssh_pre_flight.sh')} + self.test_script = os.path.join(RUNTIME_VARS.TMP, + 'test-pre-flight-script-worked.txt') + + def _create_roster(self): + self.custom_roster(self.roster, self.data) + + with salt.utils.files.fopen(self.data['ssh_pre_flight'], 'w') as fp_: + fp_.write('touch {0}'.format(self.test_script)) + + def test_ssh_pre_flight(self): + ''' + test ssh when ssh_pre_flight is set + ensure the script runs successfully + ''' + self._create_roster() + ret = self.run_function('test.ping', roster_file=self.roster) + + assert os.path.exists(self.test_script) + + def test_ssh_run_pre_flight(self): + ''' + test ssh when --pre-flight is passed to salt-ssh + to ensure the script runs successfully + ''' + self._create_roster() + # make sure we previously ran a command so the thin dir exists + self.run_function('test.ping', wipe=False) + assert not os.path.exists(self.test_script) + + ret = self.run_function('test.ping', ssh_opts='--pre-flight', + roster_file=self.roster, wipe=False) + assert os.path.exists(self.test_script) + + def tearDown(self): + ''' + make sure to clean up any old ssh directories + ''' + files = [self.roster, self.data['ssh_pre_flight'], self.test_script] + for fp_ in files: + if os.path.exists(fp_): + os.remove(fp_) diff --git a/tests/support/case.py b/tests/support/case.py index d9f742817f1..72ce9ddff42 100644 --- a/tests/support/case.py +++ b/tests/support/case.py @@ -85,19 +85,21 @@ class ShellTestCase(TestCase, AdaptedConfigurationTestCaseMixin, ScriptPathMixin ) def run_ssh(self, arg_str, with_retcode=False, timeout=25, - catch_stderr=False, wipe=False, raw=False, roster_file=None, **kwargs): + catch_stderr=False, wipe=False, raw=False, roster_file=None, + ssh_opts='', **kwargs): ''' Execute salt-ssh ''' if not roster_file: roster_file = os.path.join(RUNTIME_VARS.TMP_CONF_DIR, 'roster') - arg_str = '{0} {1} -c {2} -i --priv {3} --roster-file {4} localhost {5} --out=json'.format( + arg_str = '{0} {1} -c {2} -i --priv {3} --roster-file {4} {5} localhost {6} --out=json'.format( ' -W' if wipe else '', ' -r' if raw else '', self.config_dir, os.path.join(RUNTIME_VARS.TMP_CONF_DIR, 'key_test'), roster_file, + ssh_opts, arg_str ) return self.run_script( @@ -524,19 +526,21 @@ class ShellCase(ShellTestCase, AdaptedConfigurationTestCaseMixin, ScriptPathMixi return ret def run_ssh(self, arg_str, with_retcode=False, catch_stderr=False, # pylint: disable=W0221 - timeout=RUN_TIMEOUT, wipe=True, raw=False, roster_file=None, **kwargs): + timeout=RUN_TIMEOUT, wipe=True, raw=False, roster_file=None, + ssh_opts='', **kwargs): ''' Execute salt-ssh ''' if not roster_file: roster_file = os.path.join(RUNTIME_VARS.TMP_CONF_DIR, 'roster') - arg_str = '{0} -ldebug{1} -c {2} -i --priv {3} --roster-file {4} --out=json localhost {5}'.format( + arg_str = '{0} -ldebug{1} -c {2} -i --priv {3} --roster-file {4} {5} --out=json localhost {6}'.format( ' -W' if wipe else '', ' -r' if raw else '', self.config_dir, os.path.join(RUNTIME_VARS.TMP_CONF_DIR, 'key_test'), roster_file, + ssh_opts, arg_str) ret = self.run_script('salt-ssh', arg_str, diff --git a/tests/unit/client/test_ssh.py b/tests/unit/client/test_ssh.py index 2437bacbc7c..52a38f524d0 100644 --- a/tests/unit/client/test_ssh.py +++ b/tests/unit/client/test_ssh.py @@ -177,7 +177,7 @@ class SSHSingleTests(TestCase): mock_cmd = MagicMock(return_value=cmd_ret) patch_flight = patch('salt.client.ssh.Single.run_ssh_pre_flight', mock_flight) patch_cmd = patch('salt.client.ssh.Single.cmd_block', mock_cmd) - patch_os = patch('os.path.exists', side_effect=[False, True]) + patch_os = patch('os.path.exists', side_effect=[True]) with patch_os, patch_flight, patch_cmd: ret = single.run() @@ -207,7 +207,7 @@ class SSHSingleTests(TestCase): mock_cmd = MagicMock(return_value=cmd_ret) patch_flight = patch('salt.client.ssh.Single.run_ssh_pre_flight', mock_flight) patch_cmd = patch('salt.client.ssh.Single.cmd_block', mock_cmd) - patch_os = patch('os.path.exists', side_effect=[False, True]) + patch_os = patch('os.path.exists', side_effect=[True]) with patch_os, patch_flight, patch_cmd: ret = single.run() @@ -237,7 +237,7 @@ class SSHSingleTests(TestCase): mock_cmd = MagicMock(return_value=cmd_ret) patch_flight = patch('salt.client.ssh.Single.run_ssh_pre_flight', mock_flight) patch_cmd = patch('salt.client.ssh.Single.cmd_block', mock_cmd) - patch_os = patch('os.path.exists', side_effect=[False, False]) + patch_os = patch('os.path.exists', side_effect=[False]) with patch_os, patch_flight, patch_cmd: ret = single.run() @@ -262,11 +262,11 @@ class SSHSingleTests(TestCase): mine=False, **target) - cmd_ret = ('', 'Error running script', 1) + cmd_ret = ('', '', 0) mock_flight = MagicMock(return_value=cmd_ret) mock_cmd = MagicMock(return_value=cmd_ret) patch_flight = patch('salt.client.ssh.Single.run_ssh_pre_flight', mock_flight) - patch_cmd = patch('salt.client.ssh.Single.cmd_block', mock_cmd) + patch_cmd = patch('salt.client.ssh.shell.Shell.exec_cmd', mock_cmd) patch_os = patch('os.path.exists', return_value=True) with patch_os, patch_flight, patch_cmd: