Introduce 'rabbitmq-diagnostics check_port_connectivity'
This commit is contained in:
		
							parent
							
								
									ad23b37e3f
								
							
						
					
					
						commit
						e8729c5de0
					
				
							
								
								
									
										105
									
								
								deps/rabbitmq_cli/lib/rabbitmq/cli/diagnostics/commands/check_port_connectivity_command.ex
								
								
								
									vendored
								
								
									Normal file
								
							
							
						
						
									
										105
									
								
								deps/rabbitmq_cli/lib/rabbitmq/cli/diagnostics/commands/check_port_connectivity_command.ex
								
								
								
									vendored
								
								
									Normal file
								
							| 
						 | 
					@ -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
 | 
				
			||||||
| 
						 | 
					@ -14,6 +14,7 @@
 | 
				
			||||||
## Copyright (c) 2007-2019 Pivotal Software, Inc.  All rights reserved.
 | 
					## Copyright (c) 2007-2019 Pivotal Software, Inc.  All rights reserved.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
defmodule RabbitMQ.CLI.Diagnostics.Helpers do
 | 
					defmodule RabbitMQ.CLI.Diagnostics.Helpers do
 | 
				
			||||||
 | 
					  import Record, only: [defrecord: 2, extract: 2]
 | 
				
			||||||
  import RabbitCommon.Records
 | 
					  import RabbitCommon.Records
 | 
				
			||||||
  import Rabbitmq.Atom.Coerce
 | 
					  import Rabbitmq.Atom.Coerce
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,6 +22,8 @@ defmodule RabbitMQ.CLI.Diagnostics.Helpers do
 | 
				
			||||||
  # Listeners
 | 
					  # Listeners
 | 
				
			||||||
  #
 | 
					  #
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defrecord :hostent, extract(:hostent, from_lib: "kernel/include/inet.hrl")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def listeners_on(listeners, target_node) do
 | 
					  def listeners_on(listeners, target_node) do
 | 
				
			||||||
    Enum.filter(listeners, fn listener(node: node) ->
 | 
					    Enum.filter(listeners, fn listener(node: node) ->
 | 
				
			||||||
      node == target_node
 | 
					      node == target_node
 | 
				
			||||||
| 
						 | 
					@ -34,14 +37,14 @@ defmodule RabbitMQ.CLI.Diagnostics.Helpers do
 | 
				
			||||||
    end)
 | 
					    end)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def listener_maps(listeners) do
 | 
					  def listener_map(listener) 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
 | 
					    # Listener options are left out intentionally: they can contain deeply nested values
 | 
				
			||||||
    # that are impossible to serialise to JSON.
 | 
					    # that are impossible to serialise to JSON.
 | 
				
			||||||
    #
 | 
					    #
 | 
				
			||||||
    # Management plugin/HTTP API had its fair share of bugs because of that
 | 
					    # 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
 | 
					    # and now filters out a lot of options. Raw listener data can be seen in
 | 
				
			||||||
    # rabbitmq-diagnostics status.
 | 
					    # rabbitmq-diagnostics status.
 | 
				
			||||||
 | 
					    listener(node: node, protocol: protocol, ip_address: interface, port: port) = listener
 | 
				
			||||||
    %{
 | 
					    %{
 | 
				
			||||||
      node: node,
 | 
					      node: node,
 | 
				
			||||||
      protocol: protocol,
 | 
					      protocol: protocol,
 | 
				
			||||||
| 
						 | 
					@ -50,6 +53,9 @@ defmodule RabbitMQ.CLI.Diagnostics.Helpers do
 | 
				
			||||||
      purpose: protocol_label(to_atom(protocol))
 | 
					      purpose: protocol_label(to_atom(protocol))
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def listener_maps(listeners) do
 | 
				
			||||||
 | 
					    Enum.map(listeners, &listener_map/1)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def listener_rows(listeners) do
 | 
					  def listener_rows(listeners) do
 | 
				
			||||||
| 
						 | 
					@ -63,6 +69,25 @@ defmodule RabbitMQ.CLI.Diagnostics.Helpers do
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  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
 | 
					  def normalize_protocol(proto) do
 | 
				
			||||||
    val = proto |> to_string |> String.downcase
 | 
					    val = proto |> to_string |> String.downcase
 | 
				
			||||||
    case val do
 | 
					    case val do
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,9 +19,7 @@ defmodule CloseAllConnectionsCommandTest do
 | 
				
			||||||
  import TestHelper
 | 
					  import TestHelper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  alias RabbitMQ.CLI.Ctl.RpcStream
 | 
					  alias RabbitMQ.CLI.Ctl.RpcStream
 | 
				
			||||||
 | 
					 | 
				
			||||||
  @helpers RabbitMQ.CLI.Core.Helpers
 | 
					  @helpers RabbitMQ.CLI.Core.Helpers
 | 
				
			||||||
 | 
					 | 
				
			||||||
  @command RabbitMQ.CLI.Ctl.Commands.CloseAllConnectionsCommand
 | 
					  @command RabbitMQ.CLI.Ctl.Commands.CloseAllConnectionsCommand
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @vhost "/"
 | 
					  @vhost "/"
 | 
				
			||||||
| 
						 | 
					@ -33,8 +31,6 @@ defmodule CloseAllConnectionsCommandTest do
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    on_exit([], fn ->
 | 
					    on_exit([], fn ->
 | 
				
			||||||
      close_all_connections(get_rabbit_hostname())
 | 
					      close_all_connections(get_rabbit_hostname())
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    end)
 | 
					    end)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :ok
 | 
					    :ok
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
				
			||||||
		Loading…
	
		Reference in New Issue