diff options
author | Michael Klishin <mklishin@pivotal.io> | 2019-01-23 22:30:20 +0300 |
---|---|---|
committer | Michael Klishin <mklishin@pivotal.io> | 2019-01-23 22:30:20 +0300 |
commit | e8729c5de0ab41bce6d459f2948cef655cfe7059 (patch) | |
tree | ad40178b45f9d049aa6bac0e8ab9d3fe9084a4fd | |
parent | ad23b37e3f60e62dfda5b296794817c2df088521 (diff) | |
download | rabbitmq-server-git-e8729c5de0ab41bce6d459f2948cef655cfe7059.tar.gz |
Introduce 'rabbitmq-diagnostics check_port_connectivity'
4 files changed, 215 insertions, 21 deletions
diff --git a/deps/rabbitmq_cli/lib/rabbitmq/cli/diagnostics/commands/check_port_connectivity_command.ex b/deps/rabbitmq_cli/lib/rabbitmq/cli/diagnostics/commands/check_port_connectivity_command.ex new file mode 100644 index 0000000000..c910322bb1 --- /dev/null +++ b/deps/rabbitmq_cli/lib/rabbitmq/cli/diagnostics/commands/check_port_connectivity_command.ex @@ -0,0 +1,105 @@ +## The contents of this file are subject to the Mozilla Public License +## Version 1.1 (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.mozilla.org/MPL/ +## +## Software distributed under the License is distributed on an "AS IS" +## basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +## the License for the specific language governing rights and +## limitations under the License. +## +## The Original Code is RabbitMQ. +## +## The Initial Developer of the Original Code is GoPivotal, Inc. +## Copyright (c) 2007-2019 Pivotal Software, Inc. All rights reserved. + +defmodule RabbitMQ.CLI.Diagnostics.Commands.CheckPortConnectivityCommand do + @moduledoc """ + Displays all listeners on a node. + + Returns a code of 0 unless there were connectivity and authentication + errors. This command is not meant to be used in health checks. + """ + + alias RabbitMQ.CLI.Core.Helpers + import RabbitMQ.CLI.Diagnostics.Helpers, only: [listeners_on: 2, + listener_lines: 1, + listener_map: 1, + listener_maps: 1, + check_listener_connectivity: 3] + + @behaviour RabbitMQ.CLI.CommandBehaviour + + @default_timeout 30_000 + + use RabbitMQ.CLI.Core.AcceptsDefaultSwitchesAndTimeout + + def merge_defaults(args, opts) do + timeout = case opts[:timeout] do + nil -> @default_timeout; + :infinity -> @default_timeout; + other -> other + end + {args, Map.merge(opts, %{timeout: timeout})} + end + + use RabbitMQ.CLI.Core.AcceptsNoPositionalArguments + use RabbitMQ.CLI.Core.RequiresRabbitAppRunning + + def run([], %{node: node_name, timeout: timeout}) do + case :rabbit_misc.rpc_call(node_name, + :rabbit_networking, :active_listeners, [], timeout) do + {:error, _} = err -> err + {:error, _, _} = err -> err + xs when is_list(xs) -> + locals = listeners_on(xs, node_name) + case locals do + [] -> {true, locals} + _ -> check_connectivity_of(locals, node_name, timeout) + end; + other -> other + end + end + + def output({true, listeners}, %{node: node_name, formatter: "json"}) do + {:ok, %{"result" => "ok", + "node" => node_name, + "listeners" => listener_maps(listeners)}} + end + def output({true, listeners}, %{node: node_name}) do + ports = listeners |> listener_maps |> Enum.map(fn %{port: p} -> p end) + |> Enum.sort |> Enum.join(", ") + {:ok, "Successfully connected to ports #{ports} on node #{node_name}."} + end + def output({false, failures}, %{formatter: "json", node: node_name}) do + {:error, %{"result" => "error", + "node" => node_name, + "failures" => listener_maps(failures)}} + end + def output({false, failures}, %{node: node_name}) do + lines = ["Connection to ports of the following listeners on node #{node_name} failed: " + | listener_lines(failures)] + {:error, Enum.join(lines, Helpers.line_separator())} + end + + def usage, do: "check_port_connectivity" + + def banner([], %{node: node_name}) do + "Testing TCP connections to all active listeners on node #{node_name} ..." + end + + # + # Implementation + # + + defp check_connectivity_of(listeners, node_name, timeout) do + # per listener timeout + t = Kernel.trunc(timeout / (length(listeners) + 1)) + failures = Enum.reject(listeners, + fn l -> check_listener_connectivity(listener_map(l), node_name, t) end) + case failures do + [] -> {true, listeners} + fs -> {false, fs} + end + end +end diff --git a/deps/rabbitmq_cli/lib/rabbitmq/cli/diagnostics/diagnostics_helpers.ex b/deps/rabbitmq_cli/lib/rabbitmq/cli/diagnostics/diagnostics_helpers.ex index 4ba1a134ed..fe8098832a 100644 --- a/deps/rabbitmq_cli/lib/rabbitmq/cli/diagnostics/diagnostics_helpers.ex +++ b/deps/rabbitmq_cli/lib/rabbitmq/cli/diagnostics/diagnostics_helpers.ex @@ -14,6 +14,7 @@ ## Copyright (c) 2007-2019 Pivotal Software, Inc. All rights reserved. defmodule RabbitMQ.CLI.Diagnostics.Helpers do + import Record, only: [defrecord: 2, extract: 2] import RabbitCommon.Records import Rabbitmq.Atom.Coerce @@ -21,6 +22,8 @@ defmodule RabbitMQ.CLI.Diagnostics.Helpers do # Listeners # + defrecord :hostent, extract(:hostent, from_lib: "kernel/include/inet.hrl") + def listeners_on(listeners, target_node) do Enum.filter(listeners, fn listener(node: node) -> node == target_node @@ -34,22 +37,25 @@ defmodule RabbitMQ.CLI.Diagnostics.Helpers do end) end + def listener_map(listener) do + # Listener options are left out intentionally: they can contain deeply nested values + # that are impossible to serialise to JSON. + # + # Management plugin/HTTP API had its fair share of bugs because of that + # and now filters out a lot of options. Raw listener data can be seen in + # rabbitmq-diagnostics status. + listener(node: node, protocol: protocol, ip_address: interface, port: port) = listener + %{ + node: node, + protocol: protocol, + interface: :inet.ntoa(interface) |> to_string |> maybe_enquote_interface, + port: port, + purpose: protocol_label(to_atom(protocol)) + } + end + def listener_maps(listeners) do - for listener(node: node, protocol: protocol, ip_address: interface, port: port) <- listeners do - # Listener options are left out intentionally: they can contain deeply nested values - # that are impossible to serialise to JSON. - # - # Management plugin/HTTP API had its fair share of bugs because of that - # and now filters out a lot of options. Raw listener data can be seen in - # rabbitmq-diagnostics status. - %{ - node: node, - protocol: protocol, - interface: :inet.ntoa(interface) |> to_string |> maybe_enquote_interface, - port: port, - purpose: protocol_label(to_atom(protocol)) - } - end + Enum.map(listeners, &listener_map/1) end def listener_rows(listeners) do @@ -63,6 +69,25 @@ defmodule RabbitMQ.CLI.Diagnostics.Helpers do end end + def check_port_connectivity(port, node_name, timeout) do + hostname = Regex.replace(~r/^(.+)@/, to_string(node_name), "") |> to_charlist + try do + case :gen_tcp.connect(hostname, port, [], timeout) do + {:error, _} -> false + {:ok, port} -> + :ok = :gen_tcp.close(port) + true + end + # `gen_tcp:connect/4` will throw if the port is outside of its + # expected domain + catch :exit, _ -> false + end + end + + def check_listener_connectivity(%{port: port}, node_name, timeout) do + check_port_connectivity(port, node_name, timeout) + end + def normalize_protocol(proto) do val = proto |> to_string |> String.downcase case val do diff --git a/deps/rabbitmq_cli/test/close_all_connections_command_test.exs b/deps/rabbitmq_cli/test/close_all_connections_command_test.exs index d71f557292..7bedfe7a65 100644 --- a/deps/rabbitmq_cli/test/close_all_connections_command_test.exs +++ b/deps/rabbitmq_cli/test/close_all_connections_command_test.exs @@ -19,9 +19,7 @@ defmodule CloseAllConnectionsCommandTest do import TestHelper alias RabbitMQ.CLI.Ctl.RpcStream - @helpers RabbitMQ.CLI.Core.Helpers - @command RabbitMQ.CLI.Ctl.Commands.CloseAllConnectionsCommand @vhost "/" @@ -33,8 +31,6 @@ defmodule CloseAllConnectionsCommandTest do on_exit([], fn -> close_all_connections(get_rabbit_hostname()) - - end) :ok @@ -140,7 +136,7 @@ defmodule CloseAllConnectionsCommandTest do defp fetch_connection_vhosts(node, nodes) do fetch_connection_vhosts(node, nodes, 10) end - + defp fetch_connection_vhosts(node, nodes, retries) do stream = RpcStream.receive_list_items(node, :rabbit_networking, @@ -159,7 +155,7 @@ defmodule CloseAllConnectionsCommandTest do fetch_connection_vhosts(node, nodes, retries - 1) _ -> xs - end + end end end diff --git a/deps/rabbitmq_cli/test/diagnostics/check_port_connectivity_command_test.exs b/deps/rabbitmq_cli/test/diagnostics/check_port_connectivity_command_test.exs new file mode 100644 index 0000000000..e3ef7c67c2 --- /dev/null +++ b/deps/rabbitmq_cli/test/diagnostics/check_port_connectivity_command_test.exs @@ -0,0 +1,68 @@ +## The contents of this file are subject to the Mozilla Public License +## Version 1.1 (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.mozilla.org/MPL/ +## +## Software distributed under the License is distributed on an "AS IS" +## basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +## the License for the specific language governing rights and +## limitations under the License. +## +## The Original Code is RabbitMQ. +## +## The Initial Developer of the Original Code is GoPivotal, Inc. +## Copyright (c) 2007-2019 Pivotal Software, Inc. All rights reserved. + +defmodule CheckPortConnectivityCommandTest do + use ExUnit.Case + import TestHelper + + @command RabbitMQ.CLI.Diagnostics.Commands.CheckPortConnectivityCommand + + setup_all do + RabbitMQ.CLI.Core.Distribution.start() + + :ok + end + + setup context do + {:ok, opts: %{ + node: get_rabbit_hostname(), + timeout: context[:test_timeout] || 30000 + }} + end + + test "merge_defaults: provides a default timeout" do + assert @command.merge_defaults([], %{}) == {[], %{timeout: 30000}} + end + + test "validate: treats positional arguments as a failure" do + assert @command.validate(["extra-arg"], %{}) == {:validation_failure, :too_many_args} + end + + test "validate: treats empty positional arguments and default switches as a success" do + assert @command.validate([], %{}) == :ok + end + + @tag test_timeout: 3000 + test "run: targeting an unreachable node throws a badrpc", context do + assert @command.run([], Map.merge(context[:opts], %{node: :jake@thedog})) == {:badrpc, :nodedown} + end + + test "run: tries to connect to every inferred active listener", context do + assert match?({true, _}, @command.run([], context[:opts])) + end + + + test "output: when all connections succeeded, returns a success", context do + assert match?({:ok, _}, @command.output({true, []}, context[:opts])) + end + + # note: it's run/2 that filters out non-local alarms + test "output: when target node has a local alarm in effect, returns a failure", context do + failure = {:listener, :rabbit@mercurio, :lolz, 'mercurio', + {0, 0, 0, 0, 0, 0, 0, 0}, 7761613, + [backlog: 128, nodelay: true]} + assert match?({:error, _}, @command.output({false, [failure]}, context[:opts])) + end +end |