Revert product checker commits:

023cf5380f: "[7.x] Document 'UnsupportedProductError'"
	d381491482: "[7.x] Rename product error to 'UnsupportedProductError'"
	b56fbcb155: "Document the Elasticsearch product check"
	b63d005138: "Don't swallow unexpected errors during Elasticsearch verification"
	801a839093: "Verify we're connected to Elasticsearch before requests"

Signed-off-by: Shephali Mittal <[email protected]>
This commit is contained in:
Shephali Mittal
2021-08-06 12:17:51 +05:30
parent 18b39afcbf
commit 934ea8cc5e
13 changed files with 6 additions and 1192 deletions
@@ -28,7 +28,7 @@ import pytest
from mock import patch
from multidict import CIMultiDict
from elasticsearch import AIOHttpConnection, AsyncElasticsearch, __versionstr__
from elasticsearch import AIOHttpConnection, __versionstr__
from elasticsearch.compat import reraise_exceptions
from elasticsearch.exceptions import ConnectionError
@@ -410,9 +410,3 @@ class TestConnectionHttpbin:
conn = AIOHttpConnection("not.a.host.name")
with pytest.raises(ConnectionError):
await conn.perform_request("GET", "/")
async def test_elasticsearch_connection_error(self):
es = AsyncElasticsearch("http://not.a.host.name")
with pytest.raises(ConnectionError):
await es.search()
+1 -329
View File
@@ -28,16 +28,7 @@ from mock import patch
from elasticsearch import AsyncTransport
from elasticsearch.connection import Connection
from elasticsearch.connection_pool import DummyConnectionPool
from elasticsearch.exceptions import (
AuthenticationException,
AuthorizationException,
ConnectionError,
ElasticsearchWarning,
NotFoundError,
TransportError,
UnsupportedProductError,
)
from elasticsearch.transport import _ProductChecker
from elasticsearch.exceptions import ConnectionError, TransportError
pytestmark = pytest.mark.asyncio
@@ -130,7 +121,6 @@ class TestTransport:
async def test_request_timeout_extracted_from_params_and_passed(self):
t = AsyncTransport([{}], connection_class=DummyConnection, meta_header=False)
t._verified_elasticsearch = True
await t.perform_request("GET", "/", params={"request_timeout": 42})
assert 1 == len(t.get_connection().calls)
@@ -145,7 +135,6 @@ class TestTransport:
t = AsyncTransport(
[{}], opaque_id="app-1", connection_class=DummyConnection, meta_header=False
)
t._verified_elasticsearch = True
await t.perform_request("GET", "/")
assert 1 == len(t.get_connection().calls)
@@ -168,7 +157,6 @@ class TestTransport:
async def test_request_with_custom_user_agent_header(self):
t = AsyncTransport([{}], connection_class=DummyConnection, meta_header=False)
t._verified_elasticsearch = True
await t.perform_request(
"GET", "/", headers={"user-agent": "my-custom-value/1.2.3"}
@@ -184,7 +172,6 @@ class TestTransport:
t = AsyncTransport(
[{}], send_get_body_as="source", connection_class=DummyConnection
)
t._verified_elasticsearch = True
await t.perform_request("GET", "/", body={})
assert 1 == len(t.get_connection().calls)
@@ -194,7 +181,6 @@ class TestTransport:
t = AsyncTransport(
[{}], send_get_body_as="POST", connection_class=DummyConnection
)
t._verified_elasticsearch = True
await t.perform_request("GET", "/", body={})
assert 1 == len(t.get_connection().calls)
@@ -202,7 +188,6 @@ class TestTransport:
async def test_client_meta_header(self):
t = AsyncTransport([{}], connection_class=DummyConnection)
t._verified_elasticsearch = True
await t.perform_request("GET", "/", body={})
assert len(t.get_connection().calls) == 1
@@ -216,7 +201,6 @@ class TestTransport:
HTTP_CLIENT_META = ("dm", "1.2.3")
t = AsyncTransport([{}], connection_class=DummyConnectionWithMeta)
t._verified_elasticsearch = True
await t.perform_request("GET", "/", body={}, headers={"Custom": "header"})
assert len(t.get_connection().calls) == 1
@@ -229,7 +213,6 @@ class TestTransport:
async def test_client_meta_header_not_sent(self):
t = AsyncTransport([{}], meta_header=False, connection_class=DummyConnection)
t._verified_elasticsearch = True
await t.perform_request("GET", "/", body={})
assert len(t.get_connection().calls) == 1
@@ -238,7 +221,6 @@ class TestTransport:
async def test_body_gets_encoded_into_bytes(self):
t = AsyncTransport([{}], connection_class=DummyConnection)
t._verified_elasticsearch = True
await t.perform_request("GET", "/", body="你好")
assert 1 == len(t.get_connection().calls)
@@ -251,7 +233,6 @@ class TestTransport:
async def test_body_bytes_get_passed_untouched(self):
t = AsyncTransport([{}], connection_class=DummyConnection)
t._verified_elasticsearch = True
body = b"\xe4\xbd\xa0\xe5\xa5\xbd"
await t.perform_request("GET", "/", body=body)
@@ -260,7 +241,6 @@ class TestTransport:
async def test_body_surrogates_replaced_encoded_into_bytes(self):
t = AsyncTransport([{}], connection_class=DummyConnection)
t._verified_elasticsearch = True
await t.perform_request("GET", "/", body="你好\uda6a")
assert 1 == len(t.get_connection().calls)
@@ -273,8 +253,6 @@ class TestTransport:
async def test_kwargs_passed_on_to_connections(self):
t = AsyncTransport([{"host": "google.com"}], port=123)
t._verified_elasticsearch = True
await t._async_call()
assert 1 == len(t.connection_pool.connections)
assert "http://google.com:123" == t.connection_pool.connections[0].host
@@ -282,8 +260,6 @@ class TestTransport:
async def test_kwargs_passed_on_to_connection_pool(self):
dt = object()
t = AsyncTransport([{}, {}], dead_timeout=dt)
t._verified_elasticsearch = True
await t._async_call()
assert dt is t.connection_pool.dead_timeout
@@ -293,15 +269,12 @@ class TestTransport:
self.kwargs = kwargs
t = AsyncTransport([{}], connection_class=MyConnection)
t._verified_elasticsearch = True
await t._async_call()
assert 1 == len(t.connection_pool.connections)
assert isinstance(t.connection_pool.connections[0], MyConnection)
def test_add_connection(self):
t = AsyncTransport([{}], randomize_hosts=False)
t._verified_elasticsearch = True
t.add_connection({"host": "google.com", "port": 1234})
assert 2 == len(t.connection_pool.connections)
@@ -312,7 +285,6 @@ class TestTransport:
[{"exception": ConnectionError("abandon ship")}],
connection_class=DummyConnection,
)
t._verified_elasticsearch = True
connection_error = False
try:
@@ -328,7 +300,6 @@ class TestTransport:
[{"exception": ConnectionError("abandon ship")}] * 2,
connection_class=DummyConnection,
)
t._verified_elasticsearch = True
connection_error = False
try:
@@ -342,8 +313,6 @@ class TestTransport:
async def test_resurrected_connection_will_be_marked_as_live_on_success(self):
for method in ("GET", "HEAD"):
t = AsyncTransport([{}, {}], connection_class=DummyConnection)
t._verified_elasticsearch = True
await t._async_call()
con1 = t.connection_pool.get_connection()
con2 = t.connection_pool.get_connection()
@@ -369,7 +338,6 @@ class TestTransport:
connection_class=DummyConnection,
sniff_on_start=True,
)
await t._async_call()
await t.sniffing_task # Need to wait for the sniffing task to complete
@@ -383,7 +351,6 @@ class TestTransport:
sniff_on_start=True,
sniff_timeout=12,
)
await t._async_call()
await t.sniffing_task # Need to wait for the sniffing task to complete
@@ -426,8 +393,6 @@ class TestTransport:
max_retries=0,
randomize_hosts=False,
)
t._verified_elasticsearch = True
await t._async_call()
connection_error = False
@@ -452,7 +417,6 @@ class TestTransport:
max_retries=3,
randomize_hosts=False,
)
t._verified_elasticsearch = True
await t._async_init()
conn_err, conn_data = t.connection_pool.connections
@@ -468,7 +432,6 @@ class TestTransport:
connection_class=DummyConnection,
sniffer_timeout=5,
)
t._verified_elasticsearch = True
await t._async_call()
for _ in range(4):
@@ -492,9 +455,7 @@ class TestTransport:
connection_class=DummyConnection,
sniff_timeout=42,
)
t._verified_elasticsearch = True
await t._async_call()
await t.sniff_hosts()
# Ensure we parsed out the fqdn and port from the fqdn/ip:port string.
assert t.connection_pool.connection_opts[0][1] == {
@@ -511,7 +472,6 @@ class TestTransport:
connection_class=DummyConnection,
cloud_id="cluster:dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyQ0ZmE4ODIxZTc1NjM0MDMyYmVkMWNmMjIxMTBlMmY5NyQ0ZmE4ODIxZTc1NjM0MDMyYmVkMWNmMjIxMTBlMmY5Ng==",
)
t._verified_elasticsearch = True
await t._async_call()
assert not t.sniff_on_connection_fail
@@ -522,7 +482,6 @@ class TestTransport:
async def test_transport_close_closes_all_pool_connections(self):
t = AsyncTransport([{}], connection_class=DummyConnection)
t._verified_elasticsearch = True
await t._async_call()
assert not any([conn.closed for conn in t.connection_pool.connections])
@@ -530,7 +489,6 @@ class TestTransport:
assert all([conn.closed for conn in t.connection_pool.connections])
t = AsyncTransport([{}, {}], connection_class=DummyConnection)
t._verified_elasticsearch = True
await t._async_call()
assert not any([conn.closed for conn in t.connection_pool.connections])
@@ -547,7 +505,6 @@ class TestTransport:
connection_class=DummyConnection,
sniff_on_start=True,
)
t._verified_elasticsearch = True
# If our initial sniffing attempt comes back
# empty then we raise an error.
@@ -565,7 +522,6 @@ class TestTransport:
connection_class=DummyConnection,
sniff_on_start=True,
)
t._verified_elasticsearch = True
# Start the timer right before the first task
# and have a bunch of tasks come in immediately.
@@ -600,7 +556,6 @@ class TestTransport:
connection_class=DummyConnection,
sniff_on_start=True,
)
t._verified_elasticsearch = True
# Start making _async_calls() before we cancel
tasks = []
@@ -619,286 +574,3 @@ class TestTransport:
# A lot quicker than 10 seconds defined in 'delay'
assert duration < 1
@pytest.mark.parametrize(
["headers", "data"],
[
(
{},
'{"version":{"number":"6.99.0"},"tagline":"You Know, for Search"}',
),
(
{},
'{"version":{"number":"7.13.0","build_flavor":"default"},"tagline":"You Know, for Search"}',
),
(
{"X-elastic-product": "Elasticsearch"},
'{"version":{"number":"7.14.0","build_flavor":"default"},"tagline":"You Know, for Search"}',
),
],
)
async def test_verify_elasticsearch(self, headers, data):
t = AsyncTransport(
[{"data": data, "headers": headers}], connection_class=DummyConnection
)
await t.perform_request("GET", "/_search")
assert t._verified_elasticsearch is True
calls = t.connection_pool.connections[0].calls
_ = [call[1]["headers"].pop("x-elastic-client-meta") for call in calls]
assert calls == [
(
("GET", "/"),
{
"headers": {
"accept": "application/json",
},
"timeout": None,
},
),
(
("GET", "/_search", None, None),
{
"headers": {},
"ignore": (),
"timeout": None,
},
),
]
@pytest.mark.parametrize(
"exception_cls", [AuthorizationException, AuthenticationException]
)
async def test_verify_elasticsearch_skips_on_auth_errors(self, exception_cls):
t = AsyncTransport(
[{"exception": exception_cls(exception_cls.status_code)}],
connection_class=DummyConnection,
)
with pytest.warns(ElasticsearchWarning) as warns:
with pytest.raises(exception_cls):
await t.perform_request(
"GET", "/_search", headers={"Authorization": "testme"}
)
# Assert that a warning was raised due to security privileges
assert [str(w.message) for w in warns] == [
"The client is unable to verify that the server is "
"Elasticsearch due security privileges on the server side"
]
# Assert that the cluster is "verified"
assert t._verified_elasticsearch is True
# See that the headers were passed along to the "info" request made
calls = t.connection_pool.connections[0].calls
_ = [call[1]["headers"].pop("x-elastic-client-meta") for call in calls]
assert calls == [
(
("GET", "/"),
{
"headers": {
"accept": "application/json",
"authorization": "testme",
},
"timeout": None,
},
),
(
("GET", "/_search", None, None),
{
"headers": {
"Authorization": "testme",
},
"ignore": (),
"timeout": None,
},
),
]
async def test_multiple_requests_verify_elasticsearch_success(self, event_loop):
t = AsyncTransport(
[
{
"data": '{"version":{"number":"7.13.0","build_flavor":"default"},"tagline":"You Know, for Search"}',
"delay": 1,
}
],
connection_class=DummyConnection,
)
results = []
completed_at = []
async def request_task():
try:
results.append(await t.perform_request("GET", "/_search"))
except Exception as e:
results.append(e)
completed_at.append(event_loop.time())
# Execute a bunch of requests concurrently.
tasks = []
start_time = event_loop.time()
for _ in range(10):
tasks.append(event_loop.create_task(request_task()))
await asyncio.gather(*tasks)
end_time = event_loop.time()
# Exactly 10 results completed
assert len(results) == 10
# No errors in the results
assert all(isinstance(result, dict) for result in results)
# Assert that this took longer than 2 seconds but less than 2.1 seconds
duration = end_time - start_time
assert 2 <= duration <= 2.1
# Assert that every result came after ~2 seconds, no fast completions.
assert all(
2 <= completed_time - start_time <= 2.1 for completed_time in completed_at
)
# Assert that the cluster is "verified"
assert t._verified_elasticsearch is True
# See that the first request is always 'GET /' for ES check
calls = t.connection_pool.connections[0].calls
assert calls[0][0] == ("GET", "/")
# The rest of the requests are 'GET /_search' afterwards
assert all(call[0][:2] == ("GET", "/_search") for call in calls[1:])
@pytest.mark.parametrize(
["build_flavor", "tagline", "product_error", "error_message"],
[
(
"default",
"BAD TAGLINE",
_ProductChecker.UNSUPPORTED_PRODUCT,
"The client noticed that the server is not Elasticsearch and we do not support this unknown product",
),
(
"BAD BUILD FLAVOR",
"BAD TAGLINE",
_ProductChecker.UNSUPPORTED_PRODUCT,
"The client noticed that the server is not Elasticsearch and we do not support this unknown product",
),
(
"BAD BUILD FLAVOR",
"You Know, for Search",
_ProductChecker.UNSUPPORTED_DISTRIBUTION,
"The client noticed that the server is not a supported distribution of Elasticsearch",
),
],
)
async def test_multiple_requests_verify_elasticsearch_product_error(
self, event_loop, build_flavor, tagline, product_error, error_message
):
t = AsyncTransport(
[
{
"data": '{"version":{"number":"7.13.0","build_flavor":"%s"},"tagline":"%s"}'
% (build_flavor, tagline),
"delay": 1,
}
],
connection_class=DummyConnection,
)
results = []
completed_at = []
async def request_task():
try:
results.append(await t.perform_request("GET", "/_search"))
except Exception as e:
results.append(e)
completed_at.append(event_loop.time())
# Execute a bunch of requests concurrently.
tasks = []
start_time = event_loop.time()
for _ in range(10):
tasks.append(event_loop.create_task(request_task()))
await asyncio.gather(*tasks)
end_time = event_loop.time()
# Exactly 10 results completed
assert len(results) == 10
# All results were errors
assert all(isinstance(result, UnsupportedProductError) for result in results)
assert all(str(result) == error_message for result in results)
# Assert that one request was made but not 2 requests.
duration = end_time - start_time
assert 1 <= duration <= 1.1
# Assert that every result came after ~1 seconds, no fast completions.
assert all(
1 <= completed_time - start_time <= 1.1 for completed_time in completed_at
)
# Assert that the cluster is definitely not Elasticsearch
assert t._verified_elasticsearch == product_error
# See that the first request is always 'GET /' for ES check
calls = t.connection_pool.connections[0].calls
assert calls[0][0] == ("GET", "/")
# The rest of the requests are 'GET /_search' afterwards
assert all(call[0][:2] == ("GET", "/_search") for call in calls[1:])
@pytest.mark.parametrize("error_cls", [ConnectionError, NotFoundError])
async def test_multiple_requests_verify_elasticsearch_retry_on_errors(
self, event_loop, error_cls
):
t = AsyncTransport(
[
{
"exception": error_cls(),
"delay": 0.1,
}
],
connection_class=DummyConnection,
)
results = []
completed_at = []
async def request_task():
try:
results.append(await t.perform_request("GET", "/_search"))
except Exception as e:
results.append(e)
completed_at.append(event_loop.time())
# Execute a bunch of requests concurrently.
tasks = []
start_time = event_loop.time()
for _ in range(5):
tasks.append(event_loop.create_task(request_task()))
await asyncio.gather(*tasks)
end_time = event_loop.time()
# Exactly 5 results completed
assert len(results) == 5
# All results were errors and not wrapped in 'NotElasticsearchError'
assert all(isinstance(result, error_cls) for result in results)
# Assert that 5 requests were made in total (5 transport requests per x 0.1s/conn request)
duration = end_time - start_time
assert 0.5 <= duration <= 0.6
# Assert that the cluster is still in the unknown/unverified stage.
assert t._verified_elasticsearch is None
# See that the API isn't hit, instead it's the index requests that are failing.
calls = t.connection_pool.connections[0].calls
assert len(calls) == 5
assert all(call[0] == ("GET", "/") for call in calls)
+1 -7
View File
@@ -31,7 +31,7 @@ from mock import Mock, patch
from requests.auth import AuthBase
from urllib3._collections import HTTPHeaderDict
from elasticsearch import Elasticsearch, __versionstr__
from elasticsearch import __versionstr__
from elasticsearch.compat import reraise_exceptions
from elasticsearch.connection import (
Connection,
@@ -1045,9 +1045,3 @@ class TestConnectionHttpbin:
conn = RequestsHttpConnection("not.a.host.name")
with pytest.raises(ConnectionError):
conn.perform_request("GET", "/")
def test_elasticsearch_connection_error(self):
es = Elasticsearch("http://not.a.host.name")
with pytest.raises(ConnectionError):
es.search()
+2 -517
View File
@@ -26,16 +26,8 @@ from mock import patch
from elasticsearch.connection import Connection
from elasticsearch.connection_pool import DummyConnectionPool
from elasticsearch.exceptions import (
AuthenticationException,
AuthorizationException,
ConnectionError,
ElasticsearchWarning,
NotFoundError,
TransportError,
UnsupportedProductError,
)
from elasticsearch.transport import Transport, _ProductChecker, get_host_info
from elasticsearch.exceptions import ConnectionError, TransportError
from elasticsearch.transport import Transport, get_host_info
from .test_cases import TestCase
@@ -45,14 +37,11 @@ class DummyConnection(Connection):
self.exception = kwargs.pop("exception", None)
self.status, self.data = kwargs.pop("status", 200), kwargs.pop("data", "{}")
self.headers = kwargs.pop("headers", {})
self.delay = kwargs.pop("delay", None)
self.calls = []
super(DummyConnection, self).__init__(**kwargs)
def perform_request(self, *args, **kwargs):
self.calls.append((args, kwargs))
if self.delay is not None:
time.sleep(self.delay)
if self.exception:
raise self.exception
return self.status, self.headers, self.data
@@ -129,16 +118,12 @@ class TestHostsInfoCallback(TestCase):
class TestTransport(TestCase):
def test_single_connection_uses_dummy_connection_pool(self):
t = Transport([{}])
t._verified_elasticsearch = True
self.assertIsInstance(t.connection_pool, DummyConnectionPool)
t = Transport([{"host": "localhost"}])
t._verified_elasticsearch = True
self.assertIsInstance(t.connection_pool, DummyConnectionPool)
def test_request_timeout_extracted_from_params_and_passed(self):
t = Transport([{}], meta_header=False, connection_class=DummyConnection)
t._verified_elasticsearch = True
t.perform_request("GET", "/", params={"request_timeout": 42})
self.assertEqual(1, len(t.get_connection().calls))
@@ -152,7 +137,6 @@ class TestTransport(TestCase):
t = Transport(
[{}], opaque_id="app-1", meta_header=False, connection_class=DummyConnection
)
t._verified_elasticsearch = True
t.perform_request("GET", "/")
self.assertEqual(1, len(t.get_connection().calls))
@@ -173,7 +157,6 @@ class TestTransport(TestCase):
def test_request_with_custom_user_agent_header(self):
t = Transport([{}], meta_header=False, connection_class=DummyConnection)
t._verified_elasticsearch = True
t.perform_request("GET", "/", headers={"user-agent": "my-custom-value/1.2.3"})
self.assertEqual(1, len(t.get_connection().calls))
@@ -188,7 +171,6 @@ class TestTransport(TestCase):
def test_send_get_body_as_source(self):
t = Transport([{}], send_get_body_as="source", connection_class=DummyConnection)
t._verified_elasticsearch = True
t.perform_request("GET", "/", body={})
self.assertEqual(1, len(t.get_connection().calls))
@@ -198,7 +180,6 @@ class TestTransport(TestCase):
def test_send_get_body_as_post(self):
t = Transport([{}], send_get_body_as="POST", connection_class=DummyConnection)
t._verified_elasticsearch = True
t.perform_request("GET", "/", body={})
self.assertEqual(1, len(t.get_connection().calls))
@@ -206,7 +187,6 @@ class TestTransport(TestCase):
def test_client_meta_header(self):
t = Transport([{}], connection_class=DummyConnection)
t._verified_elasticsearch = True
t.perform_request("GET", "/", body={})
self.assertEqual(1, len(t.get_connection().calls))
@@ -219,7 +199,6 @@ class TestTransport(TestCase):
HTTP_CLIENT_META = ("dm", "1.2.3")
t = Transport([{}], connection_class=DummyConnectionWithMeta)
t._verified_elasticsearch = True
t.perform_request("GET", "/", body={}, headers={"Custom": "header"})
self.assertEqual(1, len(t.get_connection().calls))
@@ -232,7 +211,6 @@ class TestTransport(TestCase):
def test_client_meta_header_not_sent(self):
t = Transport([{}], meta_header=False, connection_class=DummyConnection)
t._verified_elasticsearch = True
t.perform_request("GET", "/", body={})
self.assertEqual(1, len(t.get_connection().calls))
@@ -246,7 +224,6 @@ class TestTransport(TestCase):
def test_body_gets_encoded_into_bytes(self):
t = Transport([{}], connection_class=DummyConnection)
t._verified_elasticsearch = True
t.perform_request("GET", "/", body="你好")
self.assertEqual(1, len(t.get_connection().calls))
@@ -257,7 +234,6 @@ class TestTransport(TestCase):
def test_body_bytes_get_passed_untouched(self):
t = Transport([{}], connection_class=DummyConnection)
t._verified_elasticsearch = True
body = b"\xe4\xbd\xa0\xe5\xa5\xbd"
t.perform_request("GET", "/", body=body)
@@ -266,7 +242,6 @@ class TestTransport(TestCase):
def test_body_surrogates_replaced_encoded_into_bytes(self):
t = Transport([{}], connection_class=DummyConnection)
t._verified_elasticsearch = True
t.perform_request("GET", "/", body="你好\uda6a")
self.assertEqual(1, len(t.get_connection().calls))
@@ -277,14 +252,12 @@ class TestTransport(TestCase):
def test_kwargs_passed_on_to_connections(self):
t = Transport([{"host": "google.com"}], port=123)
t._verified_elasticsearch = True
self.assertEqual(1, len(t.connection_pool.connections))
self.assertEqual("http://google.com:123", t.connection_pool.connections[0].host)
def test_kwargs_passed_on_to_connection_pool(self):
dt = object()
t = Transport([{}, {}], dead_timeout=dt)
t._verified_elasticsearch = True
self.assertIs(dt, t.connection_pool.dead_timeout)
def test_custom_connection_class(self):
@@ -293,13 +266,11 @@ class TestTransport(TestCase):
self.kwargs = kwargs
t = Transport([{}], connection_class=MyConnection)
t._verified_elasticsearch = True
self.assertEqual(1, len(t.connection_pool.connections))
self.assertIsInstance(t.connection_pool.connections[0], MyConnection)
def test_add_connection(self):
t = Transport([{}], randomize_hosts=False)
t._verified_elasticsearch = True
t.add_connection({"host": "google.com", "port": 1234})
self.assertEqual(2, len(t.connection_pool.connections))
@@ -312,7 +283,6 @@ class TestTransport(TestCase):
[{"exception": ConnectionError("abandon ship")}],
connection_class=DummyConnection,
)
t._verified_elasticsearch = True
self.assertRaises(ConnectionError, t.perform_request, "GET", "/")
self.assertEqual(4, len(t.get_connection().calls))
@@ -322,7 +292,6 @@ class TestTransport(TestCase):
[{"exception": ConnectionError("abandon ship")}] * 2,
connection_class=DummyConnection,
)
t._verified_elasticsearch = True
self.assertRaises(ConnectionError, t.perform_request, "GET", "/")
self.assertEqual(0, len(t.connection_pool.connections))
@@ -330,7 +299,6 @@ class TestTransport(TestCase):
def test_resurrected_connection_will_be_marked_as_live_on_success(self):
for method in ("GET", "HEAD"):
t = Transport([{}, {}], connection_class=DummyConnection)
t._verified_elasticsearch = True
con1 = t.connection_pool.get_connection()
con2 = t.connection_pool.get_connection()
t.connection_pool.mark_dead(con1)
@@ -342,7 +310,6 @@ class TestTransport(TestCase):
def test_sniff_will_use_seed_connections(self):
t = Transport([{"data": CLUSTER_NODES}], connection_class=DummyConnection)
t._verified_elasticsearch = True
t.set_connections([{"data": "invalid"}])
t.sniff_hosts()
@@ -355,7 +322,6 @@ class TestTransport(TestCase):
connection_class=DummyConnection,
sniff_on_start=True,
)
t._verified_elasticsearch = True
self.assertEqual(1, len(t.connection_pool.connections))
self.assertEqual("http://1.1.1.1:123", t.get_connection().host)
@@ -366,7 +332,6 @@ class TestTransport(TestCase):
sniff_on_start=True,
sniff_timeout=12,
)
t._verified_elasticsearch = True
self.assertEqual(
(("GET", "/_nodes/_all/http"), {"timeout": None}),
t.seed_connections[0].calls[0],
@@ -378,7 +343,6 @@ class TestTransport(TestCase):
connection_class=DummyConnection,
sniff_timeout=42,
)
t._verified_elasticsearch = True
t.sniff_hosts()
self.assertEqual(
(("GET", "/_nodes/_all/http"), {"timeout": 42}),
@@ -391,7 +355,6 @@ class TestTransport(TestCase):
connection_class=DummyConnection,
randomize_hosts=False,
)
t._verified_elasticsearch = True
connection = t.connection_pool.connections[1]
t.sniff_hosts()
@@ -406,7 +369,6 @@ class TestTransport(TestCase):
max_retries=0,
randomize_hosts=False,
)
t._verified_elasticsearch = True
self.assertRaises(ConnectionError, t.perform_request, "GET", "/")
self.assertEqual(1, len(t.connection_pool.connections))
@@ -422,7 +384,6 @@ class TestTransport(TestCase):
max_retries=3,
randomize_hosts=False,
)
t._verified_elasticsearch = True
conn_err, conn_data = t.connection_pool.connections
response = t.perform_request("GET", "/")
@@ -437,7 +398,6 @@ class TestTransport(TestCase):
connection_class=DummyConnection,
sniffer_timeout=5,
)
t._verified_elasticsearch = True
for _ in range(4):
t.perform_request("GET", "/")
@@ -458,7 +418,6 @@ class TestTransport(TestCase):
connection_class=DummyConnection,
sniff_timeout=42,
)
t._verified_elasticsearch = True
t.sniff_hosts()
# Ensure we parsed out the fqdn and port from the fqdn/ip:port string.
self.assertEqual(
@@ -474,480 +433,6 @@ class TestTransport(TestCase):
sniff_on_connection_fail=True,
cloud_id="cluster:dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyQ0ZmE4ODIxZTc1NjM0MDMyYmVkMWNmMjIxMTBlMmY5NyQ0ZmE4ODIxZTc1NjM0MDMyYmVkMWNmMjIxMTBlMmY5Ng==",
)
t._verified_elasticsearch = True
self.assertFalse(t.sniff_on_connection_fail)
self.assertIs(sniff_hosts.call_args, None) # Assert not called.
TAGLINE = "You Know, for Search"
@pytest.mark.parametrize(
["headers", "response", "product_error"],
[
# All empty.
({}, {}, _ProductChecker.UNSUPPORTED_PRODUCT),
# Don't check the product header immediately, need to check version first.
(
{"x-elastic-product": "Elasticsearch"},
{},
_ProductChecker.UNSUPPORTED_PRODUCT,
),
# Version not there.
({}, {"tagline": TAGLINE}, _ProductChecker.UNSUPPORTED_PRODUCT),
# Version is nonsense
(
{},
{"version": "1.0.0", "tagline": TAGLINE},
_ProductChecker.UNSUPPORTED_PRODUCT,
),
# Version number not there
({}, {"version": {}, "tagline": TAGLINE}, _ProductChecker.UNSUPPORTED_PRODUCT),
# Version number is nonsense
(
{},
{"version": {"number": "nonsense"}, "tagline": TAGLINE},
_ProductChecker.UNSUPPORTED_PRODUCT,
),
# Version number way in the past
(
{},
{"version": {"number": "1.0.0"}, "tagline": TAGLINE},
_ProductChecker.UNSUPPORTED_PRODUCT,
),
# Version number way in the future
(
{},
{"version": {"number": "999.0.0"}, "tagline": TAGLINE},
_ProductChecker.UNSUPPORTED_PRODUCT,
),
# Build flavor not supposed to be missing
(
{},
{"version": {"number": "7.13.0"}, "tagline": TAGLINE},
_ProductChecker.UNSUPPORTED_DISTRIBUTION,
),
# Build flavor is 'oss'
(
{},
{
"version": {"number": "7.10.0", "build_flavor": "oss"},
"tagline": TAGLINE,
},
_ProductChecker.UNSUPPORTED_DISTRIBUTION,
),
# Build flavor is nonsense
(
{},
{
"version": {"number": "7.13.0", "build_flavor": "nonsense"},
"tagline": TAGLINE,
},
_ProductChecker.UNSUPPORTED_DISTRIBUTION,
),
# Tagline is nonsense
(
{},
{"version": {"number": "7.1.0-SNAPSHOT"}, "tagline": "nonsense"},
_ProductChecker.UNSUPPORTED_PRODUCT,
),
# Product header is not supposed to be missing
(
{},
{"version": {"number": "7.14.0"}, "tagline": "You Know, for Search"},
_ProductChecker.UNSUPPORTED_PRODUCT,
),
# Product header is nonsense
(
{"x-elastic-product": "nonsense"},
{"version": {"number": "7.15.0"}, "tagline": TAGLINE},
_ProductChecker.UNSUPPORTED_PRODUCT,
),
],
)
def test_verify_elasticsearch_errors(headers, response, product_error):
assert _ProductChecker.check_product(headers, response) == product_error
@pytest.mark.parametrize(
["headers", "response"],
[
({}, {"version": {"number": "6.0.0"}, "tagline": TAGLINE}),
({}, {"version": {"number": "6.99.99"}, "tagline": TAGLINE}),
(
{},
{
"version": {"number": "7.0.0", "build_flavor": "default"},
"tagline": TAGLINE,
},
),
(
{},
{
"version": {"number": "7.13.99", "build_flavor": "default"},
"tagline": TAGLINE,
},
),
(
{"x-elastic-product": "Elasticsearch"},
{
"version": {"number": "7.14.0", "build_flavor": "default"},
"tagline": TAGLINE,
},
),
(
{"x-elastic-product": "Elasticsearch"},
{
"version": {"number": "7.99.99", "build_flavor": "default"},
"tagline": TAGLINE,
},
),
(
{"x-elastic-product": "Elasticsearch"},
{
"version": {"number": "8.0.0"},
},
),
],
)
def test_verify_elasticsearch_passes(headers, response):
result = _ProductChecker.check_product(headers, response)
assert result == _ProductChecker.SUCCESS
assert result is True
@pytest.mark.parametrize(
["headers", "data"],
[
(
{},
'{"version":{"number":"6.99.0"},"tagline":"You Know, for Search"}',
),
(
{},
"""{
"name" : "io",
"cluster_name" : "elasticsearch",
"cluster_uuid" : "HaMHUswUSGGnzla8B17Iqw",
"version" : {
"number" : "7.6.0",
"build_flavor" : "default",
"build_type" : "tar",
"build_hash" : "7f634e9f44834fbc12724506cc1da681b0c3b1e3",
"build_date" : "2020-02-06T00:09:00.449973Z",
"build_snapshot" : false,
"lucene_version" : "8.4.0",
"minimum_wire_compatibility_version" : "6.8.0",
"minimum_index_compatibility_version" : "6.0.0-beta1"
},
"tagline" : "You Know, for Search"
}""",
),
(
{},
'{"version":{"number":"7.13.0","build_flavor":"default"},"tagline":"You Know, for Search"}',
),
(
{"X-elastic-product": "Elasticsearch"},
'{"version":{"number":"7.14.0","build_flavor":"default"},"tagline":"You Know, for Search"}',
),
],
)
def test_verify_elasticsearch(headers, data):
t = Transport(
[{"data": data, "headers": headers}], connection_class=DummyConnection
)
t.perform_request("GET", "/_search")
assert t._verified_elasticsearch is True
calls = t.connection_pool.connections[0].calls
_ = [call[1]["headers"].pop("x-elastic-client-meta") for call in calls]
assert calls == [
(
("GET", "/"),
{
"headers": {
"accept": "application/json",
},
"timeout": None,
},
),
(
("GET", "/_search", None, None),
{
"headers": {},
"ignore": (),
"timeout": None,
},
),
]
@pytest.mark.parametrize(
"exception_cls", [AuthorizationException, AuthenticationException]
)
def test_verify_elasticsearch_skips_on_auth_errors(exception_cls):
t = Transport(
[{"exception": exception_cls(exception_cls.status_code)}],
connection_class=DummyConnection,
)
with pytest.warns(ElasticsearchWarning) as warns:
with pytest.raises(exception_cls):
t.perform_request(
"GET",
"/_search",
headers={"Authorization": "testme"},
params={"request_timeout": 3},
)
# Assert that a warning was raised due to security privileges
assert [str(w.message) for w in warns] == [
"The client is unable to verify that the server is "
"Elasticsearch due security privileges on the server side"
]
# Assert that the cluster is "verified"
assert t._verified_elasticsearch is True
# See that the headers were passed along to the "info" request made
calls = t.connection_pool.connections[0].calls
_ = [call[1]["headers"].pop("x-elastic-client-meta") for call in calls]
assert calls == [
(
("GET", "/"),
{
"headers": {
"accept": "application/json",
"authorization": "testme",
},
"timeout": 3,
},
),
(
("GET", "/_search", {}, None),
{
"headers": {
"Authorization": "testme",
},
"ignore": (),
"timeout": 3,
},
),
]
def test_multiple_requests_verify_elasticsearch_success():
try:
import threading
except ImportError:
return pytest.skip("Requires the 'threading' module")
t = Transport(
[
{
"data": '{"version":{"number":"7.13.0","build_flavor":"default"},"tagline":"You Know, for Search"}',
"delay": 1,
}
],
connection_class=DummyConnection,
)
results = []
completed_at = []
class RequestThread(threading.Thread):
def run(self):
try:
results.append(t.perform_request("GET", "/_search"))
except Exception as e:
results.append(e)
completed_at.append(time.time())
# Execute a bunch of requests concurrently.
threads = []
start_time = time.time()
for _ in range(10):
thread = RequestThread()
thread.start()
threads.append(thread)
for thread in threads:
thread.join()
end_time = time.time()
# Exactly 10 results completed
assert len(results) == 10
# No errors in the results
assert all(isinstance(result, dict) for result in results)
# Assert that this took longer than 2 seconds but less than 2.1 seconds
duration = end_time - start_time
assert 2 <= duration <= 2.1
# Assert that every result came after ~2 seconds, no fast completions.
assert all(
2 <= completed_time - start_time <= 2.1 for completed_time in completed_at
)
# Assert that the cluster is "verified"
assert t._verified_elasticsearch is True
# See that the first request is always 'GET /' for ES check
calls = t.connection_pool.connections[0].calls
assert calls[0][0] == ("GET", "/")
# The rest of the requests are 'GET /_search' afterwards
assert all(call[0][:2] == ("GET", "/_search") for call in calls[1:])
@pytest.mark.parametrize(
["build_flavor", "tagline", "product_error", "error_message"],
[
(
"default",
"BAD TAGLINE",
_ProductChecker.UNSUPPORTED_PRODUCT,
"The client noticed that the server is not Elasticsearch and we do not support this unknown product",
),
(
"BAD BUILD FLAVOR",
"BAD TAGLINE",
_ProductChecker.UNSUPPORTED_PRODUCT,
"The client noticed that the server is not Elasticsearch and we do not support this unknown product",
),
(
"BAD BUILD FLAVOR",
"You Know, for Search",
_ProductChecker.UNSUPPORTED_DISTRIBUTION,
"The client noticed that the server is not a supported distribution of Elasticsearch",
),
],
)
def test_multiple_requests_verify_elasticsearch_product_error(
build_flavor, tagline, product_error, error_message
):
try:
import threading
except ImportError:
return pytest.skip("Requires the 'threading' module")
t = Transport(
[
{
"data": '{"version":{"number":"7.13.0","build_flavor":"%s"},"tagline":"%s"}'
% (build_flavor, tagline),
"delay": 1,
}
],
connection_class=DummyConnection,
)
results = []
completed_at = []
class RequestThread(threading.Thread):
def run(self):
try:
results.append(t.perform_request("GET", "/_search"))
except Exception as e:
results.append(e)
completed_at.append(time.time())
# Execute a bunch of requests concurrently.
threads = []
start_time = time.time()
for _ in range(10):
thread = RequestThread()
thread.start()
threads.append(thread)
for thread in threads:
thread.join()
end_time = time.time()
# Exactly 10 results completed
assert len(results) == 10
# All results were errors
assert all(isinstance(result, UnsupportedProductError) for result in results)
assert all(str(result) == error_message for result in results)
# Assert that one request was made but not 2 requests.
duration = end_time - start_time
assert 1 <= duration <= 1.1
# Assert that every result came after ~1 seconds, no fast completions.
assert all(
1 <= completed_time - start_time <= 1.1 for completed_time in completed_at
)
# Assert that the cluster is definitely not Elasticsearch
assert t._verified_elasticsearch == product_error
# See that the first request is always 'GET /' for ES check
calls = t.connection_pool.connections[0].calls
assert calls[0][0] == ("GET", "/")
# The rest of the requests are 'GET /_search' afterwards
assert all(call[0][:2] == ("GET", "/_search") for call in calls[1:])
@pytest.mark.parametrize("error_cls", [ConnectionError, NotFoundError])
def test_multiple_requests_verify_elasticsearch_retry_on_errors(error_cls):
try:
import threading
except ImportError:
return pytest.skip("Requires the 'threading' module")
t = Transport(
[
{
"exception": error_cls(),
"delay": 0.1,
}
],
connection_class=DummyConnection,
)
results = []
completed_at = []
class RequestThread(threading.Thread):
def run(self):
try:
results.append(t.perform_request("GET", "/_search"))
except Exception as e:
results.append(e)
completed_at.append(time.time())
# Execute a bunch of requests concurrently.
threads = []
start_time = time.time()
for _ in range(5):
thread = RequestThread()
thread.start()
threads.append(thread)
for thread in threads:
thread.join()
end_time = time.time()
# Exactly 5 results completed
assert len(results) == 5
# All results were errors and not wrapped in 'UnsupportedProductError'
assert all(isinstance(result, error_cls) for result in results)
# Assert that 5 requests were made in total (5 transport requests per x 0.1s/conn request)
duration = end_time - start_time
assert 0.5 <= duration <= 0.6
# Assert that the cluster is still in the unknown/unverified stage.
assert t._verified_elasticsearch is None
# See that the API isn't hit, instead it's the index requests that are failing.
calls = t.connection_pool.connections[0].calls
assert len(calls) == 5
assert all(call[0] == ("GET", "/") for call in calls)