diff --git a/changelog/68211.fixed.md b/changelog/68211.fixed.md new file mode 100644 index 000000000000..a5c76205ddc6 --- /dev/null +++ b/changelog/68211.fixed.md @@ -0,0 +1 @@ +Fixed ``salt.utils.vmware`` to use the supported ``token``/``tokenType`` arguments instead of the deprecated ``b64token``/``mechanism`` arguments when calling ``pyVim.connect.SmartConnect``. pyvmomi 9 raises an exception when either deprecated argument is truthy, which broke salt-cloud, the ``vsphere`` execution module, and other VMware integrations as soon as pyvmomi was upgraded. diff --git a/salt/utils/vmware.py b/salt/utils/vmware.py index f6590784f62c..f2401c788aee 100644 --- a/salt/utils/vmware.py +++ b/salt/utils/vmware.py @@ -272,6 +272,17 @@ def _get_service_instance( "Please check the debug log for more information.".format(host) ) + # pyvmomi >= 9 removed support for the deprecated b64token/mechanism + # keyword arguments and unconditionally raises if either is truthy. The + # supported replacement keywords are token/tokenType, present since + # pyvmomi 8. Only forward them when we actually have a token to send - + # for the default userpass mechanism the credentials are passed through + # user/pwd and no token argument is needed. + token_kwargs = {} + if token is not None: + token_kwargs["token"] = token + token_kwargs["tokenType"] = mechanism + try: if verify_ssl: service_instance = SmartConnect( @@ -280,8 +291,7 @@ def _get_service_instance( pwd=password, protocol=protocol, port=port, - b64token=token, - mechanism=mechanism, + **token_kwargs, ) except TypeError as exc: if "unexpected keyword argument" in exc.message: @@ -320,8 +330,7 @@ def _get_service_instance( protocol=protocol, port=port, sslContext=ssl._create_unverified_context(), - b64token=token, - mechanism=mechanism, + **token_kwargs, ) except Exception as exc: # pylint: disable=broad-except # pyVmomi's SmartConnect() actually raises Exception in some cases. @@ -336,8 +345,7 @@ def _get_service_instance( protocol=protocol, port=port, sslContext=context, - b64token=token, - mechanism=mechanism, + **token_kwargs, ) except Exception as exc: # pylint: disable=broad-except log.exception(exc) diff --git a/tests/pytests/unit/utils/test_vmware.py b/tests/pytests/unit/utils/test_vmware.py index 2a54271aac9c..fa17b2855c90 100644 --- a/tests/pytests/unit/utils/test_vmware.py +++ b/tests/pytests/unit/utils/test_vmware.py @@ -91,3 +91,87 @@ def test_reconnect_when_disconnecting_dead_socket_raises(connection_params): salt.utils.vmware.get_service_instance(**connection_params) assert mock_disconnect.call_count == 1 assert mock_get_si.call_count == 2 + + +def test_userpass_does_not_pass_deprecated_b64token_mechanism(): + """ + pyvmomi 9 raises Exception('b64token and mechanism are no longer + supported') as soon as either keyword argument is truthy. For the + userpass mechanism Salt has no token at all, so SmartConnect must be + called without the deprecated b64token/mechanism keywords (issue + #68211). + """ + mock_sc = MagicMock() + with patch("salt.utils.vmware.SmartConnect", mock_sc), patch( + "salt.utils.vmware.Disconnect", MagicMock() + ): + salt.utils.vmware._get_service_instance( + host="fake_host.fqdn", + username="fake_username", + password="fake_password", + protocol="fake_protocol", + port=1, + mechanism="userpass", + principal=None, + domain=None, + ) + assert mock_sc.call_count == 1 + kwargs = mock_sc.call_args.kwargs + assert "b64token" not in kwargs + assert "mechanism" not in kwargs + + +def test_sspi_uses_token_and_tokenType_not_b64token_mechanism(): + """ + pyvmomi 9 replaced the deprecated b64token/mechanism keywords with + token/tokenType. For the sspi mechanism Salt now forwards the gssapi + token through the new keyword arguments so SmartConnect does not raise + (issue #68211). + """ + mock_sc = MagicMock() + mock_token = MagicMock(return_value="fake_token") + with patch("salt.utils.vmware.SmartConnect", mock_sc), patch( + "salt.utils.vmware.get_gssapi_token", mock_token + ), patch("salt.utils.vmware.Disconnect", MagicMock()): + salt.utils.vmware._get_service_instance( + host="fake_host.fqdn", + username="fake_username", + password="fake_password", + protocol="fake_protocol", + port=1, + mechanism="sspi", + principal="fake_principal", + domain="fake_domain", + ) + assert mock_sc.call_count == 1 + kwargs = mock_sc.call_args.kwargs + assert "b64token" not in kwargs + assert "mechanism" not in kwargs + assert kwargs.get("token") == "fake_token" + assert kwargs.get("tokenType") == "sspi" + + +def test_userpass_verify_ssl_false_does_not_pass_b64token_mechanism(): + """ + The verify_ssl=False code path also must not forward the deprecated + b64token/mechanism keywords to pyvmomi 9 SmartConnect (issue #68211). + """ + mock_sc = MagicMock() + with patch("salt.utils.vmware.SmartConnect", mock_sc), patch( + "salt.utils.vmware.Disconnect", MagicMock() + ), patch("ssl._create_unverified_context", MagicMock()): + salt.utils.vmware._get_service_instance( + host="fake_host.fqdn", + username="fake_username", + password="fake_password", + protocol="fake_protocol", + port=1, + mechanism="userpass", + principal=None, + domain=None, + verify_ssl=False, + ) + assert mock_sc.call_count == 1 + kwargs = mock_sc.call_args.kwargs + assert "b64token" not in kwargs + assert "mechanism" not in kwargs diff --git a/tests/unit/utils/test_vmware.py b/tests/unit/utils/test_vmware.py index 637d34608120..852a8d68cce3 100644 --- a/tests/unit/utils/test_vmware.py +++ b/tests/unit/utils/test_vmware.py @@ -1544,8 +1544,6 @@ def test_userpass_mechanism_no_domain(self): pwd="fake_password", protocol="fake_protocol", port=1, - b64token=None, - mechanism="userpass", ) def test_userpass_mech_domain_unused(self): @@ -1567,8 +1565,6 @@ def test_userpass_mech_domain_unused(self): pwd="fake_password", protocol="fake_protocol", port=1, - b64token=None, - mechanism="userpass", ) mock_sc.reset_mock() salt.utils.vmware._get_service_instance( @@ -1587,8 +1583,6 @@ def test_userpass_mech_domain_unused(self): pwd="fake_password", protocol="fake_protocol", port=1, - b64token=None, - mechanism="userpass", ) def test_sspi_empty_principal(self): @@ -1664,8 +1658,8 @@ def test_sspi_get_token_success_(self): pwd="fake_password", protocol="fake_protocol", port=1, - b64token="fake_token", - mechanism="sspi", + token="fake_token", + tokenType="sspi", ) def test_first_attempt_successful_connection(self): @@ -1687,8 +1681,8 @@ def test_first_attempt_successful_connection(self): pwd="fake_password", protocol="fake_protocol", port=1, - b64token="fake_token", - mechanism="sspi", + token="fake_token", + tokenType="sspi", ) def test_first_attempt_successful_connection_verify_ssl_false(self): @@ -1724,8 +1718,8 @@ def test_first_attempt_successful_connection_verify_ssl_false(self): protocol="fake_protocol", port=1, sslContext=mock_ssl.return_value, - b64token="fake_token", - mechanism="sspi", + token="fake_token", + tokenType="sspi", ), ] mock_sc.assert_has_calls(calls) @@ -1764,8 +1758,8 @@ def test_second_attempt_successful_connection_verify_ssl_false(self): protocol="fake_protocol", port=1, sslContext=mock_ssl_unverif.return_value, - b64token="fake_token", - mechanism="sspi", + token="fake_token", + tokenType="sspi", ), call( host="fake_host.fqdn", @@ -1774,8 +1768,8 @@ def test_second_attempt_successful_connection_verify_ssl_false(self): protocol="fake_protocol", port=1, sslContext=mock_ssl_context.return_value, - b64token="fake_token", - mechanism="sspi", + token="fake_token", + tokenType="sspi", ), ] mock_sc.assert_has_calls(calls)