diff --git a/lib/elasticsearch/transport/transport/sniffer.rb b/lib/elasticsearch/transport/transport/sniffer.rb index 4cd61b7..b43c879 100644 --- a/lib/elasticsearch/transport/transport/sniffer.rb +++ b/lib/elasticsearch/transport/transport/sniffer.rb @@ -47,9 +47,9 @@ def hosts Timeout::timeout(timeout, SnifferTimeoutError) do nodes = transport.perform_request('GET', '_nodes/http').body - hosts = nodes['nodes'].map do |id,info| + hosts = nodes['nodes'].map do |id, info| if info[PROTOCOL] - host, port = info[PROTOCOL]['publish_address'].split(':') + host, port = parse_publish_address(info[PROTOCOL]['publish_address']) { :id => id, :name => info['name'], @@ -65,6 +65,29 @@ def hosts hosts end end + + private + + def parse_publish_address(publish_address) + # publish_address is in the format hostname/ip:port + if publish_address =~ /\// + parts = publish_address.partition('/') + [ parts[0], parse_address_port(parts[2])[1] ] + else + parse_address_port(publish_address) + end + end + + def parse_address_port(publish_address) + # address is ipv6 + if publish_address =~ /[\[\]]/ + if parts = publish_address.match(/\A\[(.+)\](?::(\d+))?\z/) + [ parts[1], parts[2] ] + end + else + publish_address.split(':') + end + end end end end diff --git a/spec/elasticsearch/transport/sniffer_spec.rb b/spec/elasticsearch/transport/sniffer_spec.rb new file mode 100644 index 0000000..3e2f6d3 --- /dev/null +++ b/spec/elasticsearch/transport/sniffer_spec.rb @@ -0,0 +1,269 @@ +# Licensed to Elasticsearch B.V. under one or more contributor +# license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright +# ownership. Elasticsearch B.V. licenses this file to you under +# the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +require 'spec_helper' + +describe Elasticsearch::Transport::Transport::Sniffer do + + let(:transport) do + double('transport').tap do |t| + allow(t).to receive(:perform_request).and_return(response) + allow(t).to receive(:options).and_return(sniffer_timeout: 2) + end + end + + let(:sniffer) do + described_class.new(transport) + end + + let(:response) do + double('response').tap do |r| + allow(r).to receive(:body).and_return(raw_response) + end + end + + let(:raw_response) do + { 'nodes' => { 'n1' => { 'http' => { 'publish_address' => publish_address } } } } + end + + let(:publish_address) do + '127.0.0.1:9250' + end + + describe '#initialize' do + + it 'has a transport instance' do + expect(sniffer.transport).to be(transport) + end + + it 'inherits the sniffer timeout from the transport object' do + expect(sniffer.timeout).to eq(2) + end + end + + describe '#timeout' do + + let(:sniffer) do + described_class.new(double('transport', options: {})) + end + + before do + sniffer.timeout = 3 + end + + it 'allows the timeout to be configured' do + expect(sniffer.timeout).to eq(3) + end + end + + describe '#hosts' do + + let(:hosts) do + sniffer.hosts + end + + context 'when the entire response is parsed' do + + let(:raw_response) do + { + "cluster_name" => "elasticsearch_test", + "nodes" => { + "N1" => { + "name" => "Node 1", + "transport_address" => "127.0.0.1:9300", + "host" => "testhost1", + "ip" => "127.0.0.1", + "version" => "7.0.0", + "roles" => [ + "master", + "data", + "ingest" + ], + "attributes" => { + "testattr" => "test" + }, + "http" => { + "bound_address" => [ + "[fe80::1]:9250", + "[::1]:9250", + "127.0.0.1:9250" + ], + "publish_address" => "127.0.0.1:9250", + "max_content_length_in_bytes" => 104857600 + } + } + } + } + end + + it 'parses the id' do + expect(sniffer.hosts[0][:id]).to eq('N1') + end + + it 'parses the name' do + expect(sniffer.hosts[0][:name]).to eq('Node 1') + end + + it 'parses the version' do + expect(sniffer.hosts[0][:version]).to eq('7.0.0') + end + + it 'parses the host' do + expect(sniffer.hosts[0][:host]).to eq('127.0.0.1') + end + + it 'parses the port' do + expect(sniffer.hosts[0][:port]).to eq('9250') + end + + it 'parses the roles' do + expect(sniffer.hosts[0][:roles]).to eq(['master', + 'data', + 'ingest']) + end + + it 'parses the attributes' do + expect(sniffer.hosts[0][:attributes]).to eq('testattr' => 'test') + end + end + + context 'when the transport protocol does not match' do + + let(:raw_response) do + { 'nodes' => { 'n1' => { 'foo' => { 'publish_address' => '127.0.0.1:9250' } } } } + end + + it 'does not parse the addresses' do + expect(hosts).to eq([]) + end + end + + context 'when a list of nodes is returned' do + + let(:raw_response) do + { 'nodes' => { 'n1' => { 'http' => { 'publish_address' => '127.0.0.1:9250' } }, + 'n2' => { 'http' => { 'publish_address' => '127.0.0.1:9251' } } } } + end + + it 'parses the response' do + expect(hosts.size).to eq(2) + end + + it 'correctly parses the hosts' do + expect(hosts[0][:host]).to eq('127.0.0.1') + expect(hosts[1][:host]).to eq('127.0.0.1') + end + + it 'correctly parses the ports' do + expect(hosts[0][:port]).to eq('9250') + expect(hosts[1][:port]).to eq('9251') + end + end + + context 'when the host and port are an ip address and port' do + + it 'parses the response' do + expect(hosts.size).to eq(1) + end + + it 'correctly parses the host' do + expect(hosts[0][:host]).to eq('127.0.0.1') + end + + it 'correctly parses the port' do + expect(hosts[0][:port]).to eq('9250') + end + end + + context 'when the host and port are a hostname and port' do + + let(:publish_address) do + 'testhost1.com:9250' + end + + let(:hosts) do + sniffer.hosts + end + + it 'parses the response' do + expect(hosts.size).to eq(1) + end + + it 'correctly parses the host' do + expect(hosts[0][:host]).to eq('testhost1.com') + end + + it 'correctly parses the port' do + expect(hosts[0][:port]).to eq('9250') + end + end + + context 'when the host and port are in the format: hostname/ip:port' do + + let(:publish_address) do + 'example.com/127.0.0.1:9250' + end + + it 'parses the response' do + expect(hosts.size).to eq(1) + end + + it 'uses the hostname' do + expect(hosts[0][:host]).to eq('example.com') + end + + it 'correctly parses the port' do + expect(hosts[0][:port]).to eq('9250') + end + end + + context 'when the address is IPv6' do + + let(:publish_address) do + '[::1]:9250' + end + + it 'parses the response' do + expect(hosts.size).to eq(1) + end + + it 'correctly parses the host' do + expect(hosts[0][:host]).to eq('::1') + end + + it 'correctly parses the port' do + expect(hosts[0][:port]).to eq('9250') + end + end + + context 'when the transport has :randomize_hosts option' do + + let(:raw_response) do + { 'nodes' => { 'n1' => { 'http' => { 'publish_address' => '127.0.0.1:9250' } }, + 'n2' => { 'http' => { 'publish_address' => '127.0.0.1:9251' } } } } + end + + before do + allow(transport).to receive(:options).and_return(randomize_hosts: true) + end + + it 'shuffles the list' do + expect(hosts.size).to eq(2) + end + end + end +end diff --git a/test/unit/sniffer_test.rb b/test/unit/sniffer_test.rb deleted file mode 100644 index c21815e..0000000 --- a/test/unit/sniffer_test.rb +++ /dev/null @@ -1,196 +0,0 @@ -# Licensed to Elasticsearch B.V. under one or more contributor -# license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright -# ownership. Elasticsearch B.V. licenses this file to you under -# the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. - -require 'test_helper' - -class Elasticsearch::Transport::Transport::SnifferTest < Minitest::Test - - class DummyTransport - include Elasticsearch::Transport::Transport::Base - def __build_connections; hosts; end - end - - def __nodes_info(json) - Elasticsearch::Transport::Transport::Response.new 200, MultiJson.load(json) - end - - DEFAULT_NODES_INFO_RESPONSE = <<-JSON - { - "cluster_name" : "elasticsearch_test", - "nodes" : { - "N1" : { - "name" : "Node 1", - "transport_address" : "127.0.0.1:9300", - "host" : "testhost1", - "ip" : "127.0.0.1", - "version" : "5.0.0", - "roles": [ - "master", - "data", - "ingest" - ], - "attributes": { - "testattr": "test" - }, - "http": { - "bound_address": [ - "[fe80::1]:9250", - "[::1]:9250", - "127.0.0.1:9250" - ], - "publish_address": "127.0.0.1:9250", - "max_content_length_in_bytes": 104857600 - } - } - } - } - JSON - - context "Sniffer" do - setup do - @transport = DummyTransport.new - @sniffer = Elasticsearch::Transport::Transport::Sniffer.new @transport - end - - should "be initialized with a transport instance" do - assert_equal @transport, @sniffer.transport - end - - should "return an array of hosts as hashes" do - @transport.expects(:perform_request).returns __nodes_info(DEFAULT_NODES_INFO_RESPONSE) - - hosts = @sniffer.hosts - - assert_equal 1, hosts.size - assert_equal '127.0.0.1', hosts.first[:host] - assert_equal '9250', hosts.first[:port] - assert_equal 'Node 1', hosts.first[:name] - end - - should "return an array of hosts as hostnames when a hostname is returned" do - @transport.expects(:perform_request).returns __nodes_info <<-JSON - { - "nodes" : { - "N1" : { - "http": { - "publish_address": "testhost1.com:9250" - } - } - } - } - JSON - - hosts = @sniffer.hosts - - assert_equal 1, hosts.size - assert_equal 'testhost1.com', hosts.first[:host] - assert_equal '9250', hosts.first[:port] - end - - should "return HTTP hosts for the HTTPS protocol in the transport" do - @transport = DummyTransport.new :options => { :protocol => 'https' } - @sniffer = Elasticsearch::Transport::Transport::Sniffer.new @transport - - @transport.expects(:perform_request).returns __nodes_info(DEFAULT_NODES_INFO_RESPONSE) - - assert_equal 1, @sniffer.hosts.size - end - - should "skip hosts without a matching transport protocol" do - @transport = DummyTransport.new - @sniffer = Elasticsearch::Transport::Transport::Sniffer.new @transport - - @transport.expects(:perform_request).returns __nodes_info <<-JSON - { - "nodes" : { - "N1" : { - "foobar": { - "publish_address": "foobar:1234" - } - } - } - } - JSON - - assert_empty @sniffer.hosts - end - - should "have configurable timeout" do - @transport = DummyTransport.new :options => { :sniffer_timeout => 0.001 } - @sniffer = Elasticsearch::Transport::Transport::Sniffer.new @transport - assert_equal 0.001, @sniffer.timeout - end - - should "have settable timeout" do - @transport = DummyTransport.new - @sniffer = Elasticsearch::Transport::Transport::Sniffer.new @transport - assert_equal 1, @sniffer.timeout - - @sniffer.timeout = 2 - assert_equal 2, @sniffer.timeout - end - - should "raise error on timeout" do - @transport.expects(:perform_request).raises(Elasticsearch::Transport::Transport::SnifferTimeoutError) - - # TODO: Try to inject sleep into `perform_request` or make this test less ridiculous anyhow... - assert_raise Elasticsearch::Transport::Transport::SnifferTimeoutError do - @sniffer.hosts - end - end - - should "randomize hosts" do - @transport = DummyTransport.new :options => { :randomize_hosts => true } - @sniffer = Elasticsearch::Transport::Transport::Sniffer.new @transport - - @transport.expects(:perform_request).returns __nodes_info <<-JSON - { - "ok" : true, - "cluster_name" : "elasticsearch_test", - "nodes" : { - "N1" : { - "name" : "Node 1", - "http_address" : "inet[/192.168.1.23:9200]" - }, - "N2" : { - "name" : "Node 2", - "http_address" : "inet[/192.168.1.23:9201]" - }, - "N3" : { - "name" : "Node 3", - "http_address" : "inet[/192.168.1.23:9202]" - }, - "N4" : { - "name" : "Node 4", - "http_address" : "inet[/192.168.1.23:9203]" - }, - "N5" : { - "name" : "Node 5", - "http_address" : "inet[/192.168.1.23:9204]" - } - } - } - JSON - - Array.any_instance.expects(:shuffle!) - - hosts = @sniffer.hosts - end - - end - -end