|
31 | 31 | ConnectionError, |
32 | 32 | ConnectionTimeout, |
33 | 33 | ElasticsearchWarning, |
34 | | - NotElasticsearchError, |
35 | 34 | SerializationError, |
36 | 35 | TransportError, |
| 36 | + UnsupportedProductError, |
37 | 37 | ) |
38 | 38 | from .serializer import DEFAULT_SERIALIZERS, Deserializer, JSONSerializer |
39 | 39 | from .utils import _client_meta_version |
@@ -214,8 +214,8 @@ def __init__( |
214 | 214 | # - 'True': Means we've verified that we're talking to Elasticsearch or |
215 | 215 | # that we can't rule out Elasticsearch due to auth issues. A warning |
216 | 216 | # will be raised if we receive 401/403. |
217 | | - # - 'False': Means we've discovered we're not talking to Elasticsearch, |
218 | | - # should raise an error in this case for every request. |
| 217 | + # - 'int': Means we're talking to an unsupported product, should raise |
| 218 | + # the corresponding error. |
219 | 219 | self._verified_elasticsearch = None |
220 | 220 |
|
221 | 221 | # Ensures that the ES verification request only fires once and that |
@@ -408,12 +408,9 @@ def perform_request(self, method, url, headers=None, params=None, body=None): |
408 | 408 | if self._verified_elasticsearch is None: |
409 | 409 | self._do_verify_elasticsearch(headers=headers, timeout=timeout) |
410 | 410 |
|
411 | | - # If '_verified_elasticsearch' is False we know we're not connected to Elasticsearch. |
412 | | - if self._verified_elasticsearch is False: |
413 | | - raise NotElasticsearchError( |
414 | | - "The client noticed that the server is not Elasticsearch " |
415 | | - "and we do not support this unknown product" |
416 | | - ) |
| 411 | + # If '_verified_elasticsearch' isn't 'True' then we raise an error. |
| 412 | + if self._verified_elasticsearch is not True: |
| 413 | + _ProductChecker.raise_error(self._verified_elasticsearch) |
417 | 414 |
|
418 | 415 | for attempt in range(self.max_retries + 1): |
419 | 416 | connection = self.get_connection() |
@@ -601,53 +598,84 @@ def _do_verify_elasticsearch(self, headers, timeout): |
601 | 598 | raise error |
602 | 599 |
|
603 | 600 | # Check the information we got back from the index request. |
604 | | - self._verified_elasticsearch = _verify_elasticsearch( |
| 601 | + self._verified_elasticsearch = _ProductChecker.check_product( |
605 | 602 | info_headers, info_response |
606 | 603 | ) |
607 | 604 |
|
608 | 605 |
|
609 | | -def _verify_elasticsearch(headers, response): |
610 | | - """Verifies that the server we're talking to is Elasticsearch. |
611 | | - Does this by checking HTTP headers and the deserialized |
612 | | - response to the 'info' API. Returns 'True' if we're verified |
613 | | - against Elasticsearch, 'False' otherwise. |
614 | | - """ |
615 | | - try: |
616 | | - version = response.get("version", {}) |
617 | | - version_number = tuple( |
618 | | - int(x) if x is not None else 999 |
619 | | - for x in re.search( |
620 | | - r"^([0-9]+)\.([0-9]+)(?:\.([0-9]+))?", version["number"] |
621 | | - ).groups() |
622 | | - ) |
623 | | - except (KeyError, TypeError, ValueError, AttributeError): |
624 | | - # No valid 'version.number' field, effectively 0.0.0 |
625 | | - version = {} |
626 | | - version_number = (0, 0, 0) |
627 | | - |
628 | | - # Check all of the fields and headers for missing/valid values. |
629 | | - try: |
630 | | - bad_tagline = response.get("tagline", None) != "You Know, for Search" |
631 | | - bad_build_flavor = version.get("build_flavor", None) != "default" |
632 | | - bad_product_header = headers.get("x-elastic-product", None) != "Elasticsearch" |
633 | | - except (AttributeError, TypeError): |
634 | | - bad_tagline = True |
635 | | - bad_build_flavor = True |
636 | | - bad_product_header = True |
637 | | - |
638 | | - if ( |
639 | | - # No version or version less than 6.x |
640 | | - version_number < (6, 0, 0) |
641 | | - # 6.x and there's a bad 'tagline' |
642 | | - or ((6, 0, 0) <= version_number < (7, 0, 0) and bad_tagline) |
643 | | - # 7.0-7.13 and there's a bad 'tagline' or 'build_flavor' |
644 | | - or ( |
645 | | - (7, 0, 0) <= version_number < (7, 14, 0) |
646 | | - and (bad_tagline or bad_build_flavor) |
647 | | - ) |
648 | | - # 7.14+ and there's a bad 'X-Elastic-Product' HTTP header |
649 | | - or ((7, 14, 0) <= version_number and bad_product_header) |
650 | | - ): |
651 | | - return False |
| 606 | +class _ProductChecker: |
| 607 | + """Class which verifies we're connected to a supported product""" |
| 608 | + |
| 609 | + # States that can be returned from 'check_product' |
| 610 | + SUCCESS = True |
| 611 | + UNSUPPORTED_PRODUCT = 2 |
| 612 | + UNSUPPORTED_DISTRIBUTION = 3 |
652 | 613 |
|
653 | | - return True |
| 614 | + @classmethod |
| 615 | + def raise_error(cls, state): |
| 616 | + # These states mean the product_check() didn't fail so do nothing. |
| 617 | + if state in (None, True): |
| 618 | + return |
| 619 | + |
| 620 | + if state == cls.UNSUPPORTED_DISTRIBUTION: |
| 621 | + message = ( |
| 622 | + "The client noticed that the server is not " |
| 623 | + "a supported distribution of Elasticsearch" |
| 624 | + ) |
| 625 | + else: # UNSUPPORTED_PRODUCT |
| 626 | + message = ( |
| 627 | + "The client noticed that the server is not Elasticsearch " |
| 628 | + "and we do not support this unknown product" |
| 629 | + ) |
| 630 | + raise UnsupportedProductError(message) |
| 631 | + |
| 632 | + @classmethod |
| 633 | + def check_product(cls, headers, response): |
| 634 | + # type: (dict[str, str], dict[str, str]) -> int |
| 635 | + """Verifies that the server we're talking to is Elasticsearch. |
| 636 | + Does this by checking HTTP headers and the deserialized |
| 637 | + response to the 'info' API. Returns one of the states above. |
| 638 | + """ |
| 639 | + try: |
| 640 | + version = response.get("version", {}) |
| 641 | + version_number = tuple( |
| 642 | + int(x) if x is not None else 999 |
| 643 | + for x in re.search( |
| 644 | + r"^([0-9]+)\.([0-9]+)(?:\.([0-9]+))?", version["number"] |
| 645 | + ).groups() |
| 646 | + ) |
| 647 | + except (KeyError, TypeError, ValueError, AttributeError): |
| 648 | + # No valid 'version.number' field, effectively 0.0.0 |
| 649 | + version = {} |
| 650 | + version_number = (0, 0, 0) |
| 651 | + |
| 652 | + # Check all of the fields and headers for missing/valid values. |
| 653 | + try: |
| 654 | + bad_tagline = response.get("tagline", None) != "You Know, for Search" |
| 655 | + bad_build_flavor = version.get("build_flavor", None) != "default" |
| 656 | + bad_product_header = ( |
| 657 | + headers.get("x-elastic-product", None) != "Elasticsearch" |
| 658 | + ) |
| 659 | + except (AttributeError, TypeError): |
| 660 | + bad_tagline = True |
| 661 | + bad_build_flavor = True |
| 662 | + bad_product_header = True |
| 663 | + |
| 664 | + # 7.0-7.13 and there's a bad 'tagline' or unsupported 'build_flavor' |
| 665 | + if (7, 0, 0) <= version_number < (7, 14, 0): |
| 666 | + if bad_tagline: |
| 667 | + return cls.UNSUPPORTED_PRODUCT |
| 668 | + elif bad_build_flavor: |
| 669 | + return cls.UNSUPPORTED_DISTRIBUTION |
| 670 | + |
| 671 | + elif ( |
| 672 | + # No version or version less than 6.x |
| 673 | + version_number < (6, 0, 0) |
| 674 | + # 6.x and there's a bad 'tagline' |
| 675 | + or ((6, 0, 0) <= version_number < (7, 0, 0) and bad_tagline) |
| 676 | + # 7.14+ and there's a bad 'X-Elastic-Product' HTTP header |
| 677 | + or ((7, 14, 0) <= version_number and bad_product_header) |
| 678 | + ): |
| 679 | + return cls.UNSUPPORTED_PRODUCT |
| 680 | + |
| 681 | + return True |
0 commit comments