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.Ctl.Commands.ExportDefinitionsCommand do
8  alias RabbitMQ.CLI.Core.{DocGuide, ExitCodes, Helpers}
9
10  @behaviour RabbitMQ.CLI.CommandBehaviour
11
12  def merge_defaults(["-"] = args, opts) do
13    {args, Map.merge(%{format: "json", silent: true}, Helpers.case_insensitive_format(opts))}
14  end
15  def merge_defaults(args, opts) do
16    {args, Map.merge(%{format: "json"}, Helpers.case_insensitive_format(opts))}
17  end
18
19  def switches(), do: [timeout: :integer, format: :string]
20  def aliases(), do: [t: :timeout]
21
22  def validate(_, %{format: format})
23      when format != "json" and format != "JSON" and format != "erlang" do
24    {:validation_failure, {:bad_argument, "Format should be either json or erlang"}}
25  end
26  def validate([], _) do
27    {:validation_failure, :not_enough_args}
28  end
29  def validate(args, _) when length(args) > 1 do
30    {:validation_failure, :too_many_args}
31  end
32  # output to stdout
33  def validate(["-"], _) do
34    :ok
35  end
36  def validate([path], _) do
37    dir = Path.dirname(path)
38    case File.exists?(dir, [raw: true]) do
39      true  -> :ok
40      false -> {:validation_failure, {:bad_argument, "Directory #{dir} does not exist"}}
41    end
42  end
43  def validate(_, _), do: :ok
44
45  use RabbitMQ.CLI.Core.RequiresRabbitAppRunning
46
47  def run(["-"], %{node: node_name, timeout: timeout}) do
48    case :rabbit_misc.rpc_call(node_name, :rabbit_definitions, :all_definitions, [], timeout) do
49      {:error, _} = err -> err
50      {:error, _, _} = err -> err
51      result -> {:ok, result}
52    end
53  end
54  def run([path], %{node: node_name, timeout: timeout, format: format}) do
55    case :rabbit_misc.rpc_call(node_name, :rabbit_definitions, :all_definitions, [], timeout) do
56      {:badrpc, _} = err -> err
57      {:error, _} = err -> err
58      {:error, _, _} = err -> err
59      result ->
60         # write to the file in run/2 because output/2 is not meant to
61         # produce side effects
62         body = serialise(result, format)
63         abs_path = Path.absname(path)
64
65         File.rm(abs_path)
66         case File.write(abs_path, body) do
67           # no output
68           :ok -> {:ok, nil}
69           {:error, :enoent}  ->
70             {:error, ExitCodes.exit_dataerr(), "Parent directory or file #{path} does not exist"}
71           {:error, :enotdir} ->
72             {:error, ExitCodes.exit_dataerr(), "Parent directory of file #{path} is not a directory"}
73           {:error, :enospc} ->
74             {:error, ExitCodes.exit_dataerr(), "No space left on device hosting #{path}"}
75           {:error, :eacces} ->
76             {:error, ExitCodes.exit_dataerr(), "No permissions to write to file #{path} or its parent directory"}
77           {:error, :eisdir} ->
78             {:error, ExitCodes.exit_dataerr(), "Path #{path} is a directory"}
79           {:error, err}     ->
80             {:error, ExitCodes.exit_dataerr(), "Could not write to file #{path}: #{err}"}
81         end
82    end
83  end
84
85  def output({:ok, nil}, _) do
86    {:ok, nil}
87  end
88  def output({:ok, result}, %{format: "json"}) when is_map(result) do
89    {:ok, serialise(result, "json")}
90  end
91  def output({:ok, result}, %{format: "erlang"}) when is_map(result) do
92    {:ok, serialise(result, "erlang")}
93  end
94  use RabbitMQ.CLI.DefaultOutput
95
96  def printer(), do: RabbitMQ.CLI.Printers.StdIORaw
97
98  def usage, do: "export_definitions <file_path | \"-\"> [--format <json | erlang>]"
99
100  def usage_additional() do
101    [
102      ["<file>", "Local file path to export to. Pass a dash (-) for stdout."],
103      ["--format", "output format to use: json or erlang"]
104    ]
105  end
106
107  def usage_doc_guides() do
108    [
109      DocGuide.definitions()
110    ]
111  end
112
113  def help_section(), do: :definitions
114
115  def description(), do: "Exports definitions in JSON or compressed Erlang Term Format."
116
117  def banner([path], %{format: fmt}), do: "Exporting definitions in #{human_friendly_format(fmt)} to a file at \"#{path}\" ..."
118
119  #
120  # Implementation
121  #
122
123  defp serialise(raw_map, "json") do
124    # make sure all runtime parameter values are maps, otherwise
125    # they will end up being a list of pairs (a keyword list/proplist)
126    # in the resulting JSON document
127    map = Map.update!(raw_map, :parameters, fn(params) ->
128      Enum.map(params, fn(param) ->
129        Map.update!(param, "value", &:rabbit_data_coercion.to_map/1)
130      end)
131    end)
132    {:ok, json} = JSON.encode(map)
133    json
134  end
135
136  defp serialise(map, "erlang") do
137    :erlang.term_to_binary(map, [{:compressed, 9}])
138  end
139
140  defp human_friendly_format("JSON"), do: "JSON"
141  defp human_friendly_format("json"), do: "JSON"
142  defp human_friendly_format("erlang"), do: "Erlang term format"
143end
144