1## This Source Code Form is subject to the terms of the Mozilla Public
2## License, v. 2.0. If a copy of the MPL was not distributed with this
3## file, You can obtain one at https://mozilla.org/MPL/2.0/.
4##
5## Copyright (c) 2007-2021 VMware, Inc. or its affiliates.  All rights reserved.
6
7defmodule RabbitMQ.CLI.Diagnostics.Commands.CheckPortConnectivityCommand do
8  @moduledoc """
9  Checks all listeners on the target node by opening a TCP connection to each
10  and immediately closing it.
11
12  Returns a code of 0 unless there were connectivity and authentication
13  errors. This command is meant to be used in health checks.
14  """
15
16  import RabbitMQ.CLI.Diagnostics.Helpers,
17    only: [check_listener_connectivity: 3]
18  import RabbitMQ.CLI.Core.Platform, only: [line_separator: 0]
19  import RabbitMQ.CLI.Core.Listeners
20
21  @behaviour RabbitMQ.CLI.CommandBehaviour
22
23  @default_timeout 30_000
24
25  use RabbitMQ.CLI.Core.AcceptsDefaultSwitchesAndTimeout
26
27  def merge_defaults(args, opts) do
28    timeout =
29      case opts[:timeout] do
30        nil -> @default_timeout
31        :infinity -> @default_timeout
32        other -> other
33      end
34
35    {args, Map.merge(opts, %{timeout: timeout})}
36  end
37
38  use RabbitMQ.CLI.Core.AcceptsNoPositionalArguments
39  use RabbitMQ.CLI.Core.RequiresRabbitAppRunning
40
41  def run([], %{node: node_name, timeout: timeout}) do
42    case :rabbit_misc.rpc_call(node_name, :rabbit_networking, :active_listeners, [], timeout) do
43      {:error, _} = err ->
44        err
45
46      {:error, _, _} = err ->
47        err
48
49      xs when is_list(xs) ->
50        locals = listeners_on(xs, node_name)
51
52        case locals do
53          [] -> {true, locals}
54          _ -> check_connectivity_of(locals, node_name, timeout)
55        end
56
57      other ->
58        other
59    end
60  end
61
62  def output({true, listeners}, %{node: node_name, formatter: "json"}) do
63    {:ok, %{"result" => "ok", "node" => node_name, "listeners" => listener_maps(listeners)}}
64  end
65
66  def output({true, listeners}, %{node: node_name}) do
67    ports =
68      listeners
69      |> listener_maps
70      |> Enum.map(fn %{port: p} -> p end)
71      |> Enum.sort()
72      |> Enum.join(", ")
73
74    {:ok, "Successfully connected to ports #{ports} on node #{node_name}."}
75  end
76
77  def output({false, failures}, %{formatter: "json", node: node_name}) do
78    {:error, %{"result" => "error", "node" => node_name, "failures" => listener_maps(failures)}}
79  end
80
81  def output({false, failures}, %{node: node_name}) do
82    lines = [
83      "Connection to ports of the following listeners on node #{node_name} failed: "
84      | listener_lines(failures)
85    ]
86
87    {:error, Enum.join(lines, line_separator())}
88  end
89
90  def description(), do: "Basic TCP connectivity health check for each listener's port on the target node"
91
92  def help_section(), do: :observability_and_health_checks
93
94  def usage, do: "check_port_connectivity"
95
96  def banner([], %{node: node_name}) do
97    "Testing TCP connections to all active listeners on node #{node_name} ..."
98  end
99
100  #
101  # Implementation
102  #
103
104  defp check_connectivity_of(listeners, node_name, timeout) do
105    # per listener timeout
106    t = Kernel.trunc(timeout / (length(listeners) + 1))
107
108    failures =
109      Enum.reject(
110        listeners,
111        fn l -> check_listener_connectivity(listener_map(l), node_name, t) end
112      )
113
114    case failures do
115      [] -> {true, listeners}
116      fs -> {false, fs}
117    end
118  end
119end
120