diff --git a/tests/login/step.sh b/tests/login/step.sh index 352a3ce934..af1f50adee 100755 --- a/tests/login/step.sh +++ b/tests/login/step.sh @@ -27,7 +27,7 @@ rlJournalStart rlAssertGrep "interactive" "output" if [ "$step" = "provision" ]; then - rlRun "grep '^ $step$' -A12 output | grep -i interactive" + rlRun "grep '^ $step$' -A13 output | grep -i interactive" elif [ "$step" = "prepare" ]; then rlRun "grep '^ $step$' -A17 output | grep -i interactive" elif [ "$step" = "execute" ]; then diff --git a/tests/unit/test_steps.py b/tests/unit/test_steps.py new file mode 100644 index 0000000000..211264fcbd --- /dev/null +++ b/tests/unit/test_steps.py @@ -0,0 +1,46 @@ +from unittest.mock import MagicMock, patch + +import pytest + +import tmt +from tmt.steps import Phase +from tmt.utils import GeneralError + + +class TestPhaseAssertFeelingSafe: + + def setup_method(self): + self.mock_logger = MagicMock() + self.phase = Phase(logger=self.mock_logger) + + @pytest.mark.parametrize( + ("tmt_version", "deprecated_version", "expect_warn", "expect_exception"), [ + ('1.30', '1.38', True, False), # warn for older version + ('1.40', '1.38', False, True), # raise exception for newer version + ('1.38', '1.38', False, True) # raise exception for same version + ]) + def test_assert_feeling_safe( + self, + tmt_version, + deprecated_version, + expect_warn, + expect_exception): + with patch.object(self.phase, 'warn') as mock_warn: + tmt.__version__ = tmt_version + + if expect_exception: + with pytest.raises(GeneralError): + self.phase.assert_feeling_safe(deprecated_version, 'Local provision plugin') + else: + self.phase.assert_feeling_safe(deprecated_version, 'Local provision plugin') + + assert mock_warn.called == expect_warn + + def test_assert_feeling_safe_feeling_safe(self): + with (patch.object(Phase, 'is_feeling_safe', True), + patch.object(self.phase, 'warn') as mock_warn): + tmt.__version__ = '1.40' + self.phase.assert_feeling_safe('1.38', 'Local provision plugin') + + # Check that warn is not called when feeling safe + assert not mock_warn.called diff --git a/tmt/cli.py b/tmt/cli.py index 0289818b0e..148d547011 100644 --- a/tmt/cli.py +++ b/tmt/cli.py @@ -255,6 +255,7 @@ def write_dl( force_dry_options = create_options_decorator(tmt.options.FORCE_DRY_OPTIONS) again_option = create_options_decorator(tmt.options.AGAIN_OPTION) fix_options = create_options_decorator(tmt.options.FIX_OPTIONS) +feeling_safe_option = create_options_decorator(tmt.options.FEELING_SAFE_OPTION) workdir_root_options = create_options_decorator(tmt.options.WORKDIR_ROOT_OPTIONS) filtering_options = create_options_decorator(tmt.options.FILTERING_OPTIONS) filtering_options_long = create_options_decorator(tmt.options.FILTERING_OPTIONS_LONG) @@ -281,17 +282,8 @@ def write_dl( dimensions or the @FILE notation to load data from provided yaml file. Can be specified multiple times. """) -@option( - '--feeling-safe', - is_flag=True, - help=""" - WARNING: with this option, tmt would be allowed to make - potentially dangerous actions. For example, some metadata - keys may cause scripts being executed on the runner. - Do not use this option unless you trust metadata consumed - by tmt, or unless you know what you are doing. - """) @verbosity_options +@feeling_safe_option @option( '--show-time', is_flag=True, diff --git a/tmt/options.py b/tmt/options.py index 65f8d245f4..04f5569804 100644 --- a/tmt/options.py +++ b/tmt/options.py @@ -144,7 +144,7 @@ def option( help='If specified, --debug and --verbose would emit logs also for these topics.') ] -# Force, dry and run again actions +# Force, dry, feeling-safe and run again actions DRY_OPTIONS: list[ClickOptionDecoratorType] = [ option( '-n', '--dry', is_flag=True, default=False, @@ -163,6 +163,19 @@ def option( help='Run again, even if already done before.'), ] +FEELING_SAFE_OPTION: list[ClickOptionDecoratorType] = [ + option( + '--feeling-safe', metavar='FEELING_SAFE', envvar='TMT_FEELING_SAFE', + is_flag=True, default=False, + help=""" + WARNING: with this option, tmt would be allowed to make + potentially dangerous actions. For example, some metadata + keys may cause scripts being executed on the runner. + Do not use this option unless you trust metadata consumed + by tmt, or unless you know what you are doing. + """) + ] + # Fix action FIX_OPTIONS: list[ClickOptionDecoratorType] = [ option('-F', '--fix', is_flag=True, help='Attempt to fix all discovered issues.') diff --git a/tmt/steps/__init__.py b/tmt/steps/__init__.py index cf110d27b7..9f78712405 100644 --- a/tmt/steps/__init__.py +++ b/tmt/steps/__init__.py @@ -206,6 +206,23 @@ def is_in_standalone_mode(self) -> bool: """ return False + def assert_feeling_safe(self, deprecated_in_version: str, subject: str) -> None: + """ + Raises a tmt.utils.ProvisionError if feeling-safe is required, but not set. + Warns when feeling-safe will be required in a future version. + :param deprecated_in_version: Version from which feeling-safe is required, e.g. '1.38'. + :param subject: Subject requiring feeling-safe, e.g. 'Local provision plugin'. + """ + if self.is_feeling_safe: + return + + if tmt.__version__ < deprecated_in_version: + self.warn(f"{subject} will require '--feeling-safe' option " + f"from version {deprecated_in_version}.") + + else: + raise tmt.utils.GeneralError(f"{subject} requires '--feeling-safe' option") + # A variable used to describe a generic type for all classes derived from Phase PhaseT = TypeVar('PhaseT', bound=Phase) @@ -1264,7 +1281,8 @@ def options(cls, how: Optional[str] = None) -> list[tmt.options.ClickOptionDecor ] + ( tmt.options.VERBOSITY_OPTIONS + tmt.options.FORCE_DRY_OPTIONS + - tmt.options.AGAIN_OPTION + tmt.options.AGAIN_OPTION + + tmt.options.FEELING_SAFE_OPTION ) @classmethod diff --git a/tmt/steps/provision/local.py b/tmt/steps/provision/local.py index c25cc3e1f4..9a68abb942 100644 --- a/tmt/steps/provision/local.py +++ b/tmt/steps/provision/local.py @@ -149,10 +149,14 @@ class ProvisionLocal(tmt.steps.provision.ProvisionPlugin[ProvisionLocalData]): .. warning:: - In general it is not recommended to run tests on your local machine + In general, it is not recommended to run tests on your local machine as there might be security risks. Run only those tests which you know are safe so that you don't destroy your laptop ;-) + From tmt version 1.38, ``--feeling-safe`` option or + ``TMT_FEELING_SAFE=True`` environment variable will + be required in order to use local provision plugin. + Example config: .. code-block:: yaml @@ -183,6 +187,8 @@ def go(self, *, logger: Optional[tmt.log.Logger] = None) -> None: data.show(verbose=self.verbosity_level, logger=self._logger) + self.assert_feeling_safe("1.38", "Local provision plugin") + if data.hardware and data.hardware.constraint: self.warn("The 'local' provision plugin does not support hardware requirements.")