summaryrefslogtreecommitdiff
path: root/deps/rabbitmq_cli/COMMAND_TUTORIAL.md
diff options
context:
space:
mode:
Diffstat (limited to 'deps/rabbitmq_cli/COMMAND_TUTORIAL.md')
-rw-r--r--deps/rabbitmq_cli/COMMAND_TUTORIAL.md459
1 files changed, 459 insertions, 0 deletions
diff --git a/deps/rabbitmq_cli/COMMAND_TUTORIAL.md b/deps/rabbitmq_cli/COMMAND_TUTORIAL.md
new file mode 100644
index 0000000000..8ead46afa7
--- /dev/null
+++ b/deps/rabbitmq_cli/COMMAND_TUTORIAL.md
@@ -0,0 +1,459 @@
+# Implementing Your Own rabbitmqctl Command
+
+## Introduction
+
+As of `3.7.0`, RabbitMQ [CLI
+tools](https://github.com/rabbitmq/rabbitmq-cli) (e.g. `rabbitmqctl`)
+allow plugin developers to extend them their own commands.
+
+The CLI is written in the [Elixir programming
+language](https://elixir-lang.org/) and commands can be implemented in
+Elixir, Erlang or any other Erlang-based language. This tutorial will
+use Elixir but also provides an Erlang example. The fundamentals are
+the same.
+
+This tutorial doesn't cover RabbitMQ plugin development process.
+To develop a new plugin you should check existing tutorials:
+
+ * [RabbitMQ Plugin Development](https://www.rabbitmq.com/plugin-development.html) (in Erlang)
+ * [Using Elixir to Write RabbitMQ Plugins](https://www.rabbitmq.com/blog/2013/06/03/using-elixir-to-write-rabbitmq-plugins/)
+
+
+## Anatomy of a RabbitMQ CLI Command
+
+A RabbitMQ CLI command is an Elixir/Erlang module that implements a
+particular [behavior](https://elixir-lang.org/getting-started/typespecs-and-behaviours.html).
+It should fulfill certain requirements in order to be discovered and load by CLI tools:
+
+ * Follow a naming convention (module name should match `RabbitMQ.CLI.(.*).Commands.(.*)Command`)
+ * Be included in a plugin application's module list (`modules` in the `.app` file)
+ * Implement `RabbitMQ.CLI.CommandBehaviour`
+
+## Implementing `RabbitMQ.CLI.CommandBehaviour` in Erlang
+
+When implementing a command in Erlang, you should add `Elixir` as a prefix to
+the module name and behaviour, because CLI is written in Elixir.
+It should match `Elixir.RabbitMQ.CLI.(.*).Commands.(.*)Command`
+And implement `Elixir.RabbitMQ.CLI.CommandBehaviour`
+
+
+## The Actual Tutorial
+
+Let's write a command, that does something simple, e.g. deleting a queue.
+We will use Elixir for that.
+
+First we need to declare a module with a behaviour, for example:
+
+```
+defmodule RabbitMQ.CLI.Ctl.Commands.DeleteQueueCommand do
+ @behaviour RabbitMQ.CLI.CommandBehaviour
+end
+```
+
+So far so good. But if we try to compile it, we'd see compilation errors:
+
+```
+warning: undefined behaviour function usage/0 (for behaviour RabbitMQ.CLI.CommandBehaviour)
+ lib/delete_queue_command.ex:1
+
+warning: undefined behaviour function banner/2 (for behaviour RabbitMQ.CLI.CommandBehaviour)
+ lib/delete_queue_command.ex:1
+
+warning: undefined behaviour function merge_defaults/2 (for behaviour RabbitMQ.CLI.CommandBehaviour)
+ lib/delete_queue_command.ex:1
+
+warning: undefined behaviour function validate/2 (for behaviour RabbitMQ.CLI.CommandBehaviour)
+ lib/delete_queue_command.ex:1
+
+warning: undefined behaviour function run/2 (for behaviour RabbitMQ.CLI.CommandBehaviour)
+ lib/delete_queue_command.ex:1
+
+warning: undefined behaviour function output/2 (for behaviour RabbitMQ.CLI.CommandBehaviour)
+ lib/delete_queue_command.ex:1
+```
+
+So some functions are missing. Let's implement them.
+
+
+### Usage: Help Section
+
+We'll start with
+the `usage/0` function, to provide command name in the help section:
+
+```
+ def usage(), do: "delete_queue queue_name [--if-empty|-e] [--if-unused|-u] [--vhost|-p vhost]"
+```
+
+### CLI Argument Parsing: Switches, Positional Arguments, Aliases
+
+We want our command to accept a `queue_name` positional argument,
+and two named arguments (flags): `if_empty` and `if_unused`,
+and a `vhost` argument with a value.
+
+We also want to specify shortcuts to our named arguments so that the user can use
+`-e` instead of `--if-empty`.
+
+We'll next implement the `switches/0` and `aliases/0` functions to let CLI know how it
+should parse command line arguments for this command:
+
+```
+ def switches(), do: [if_empty: :boolean, if_unused: :boolean]
+ def aliases(), do: [e: :if_empty, u: :is_unused]
+```
+
+Switches specify long arguments names and types, aliases specify shorter names.
+
+You might have noticed there is no `vhost` switch there. It's because `vhost` is a global
+switch and will be available to all commands in the CLI: after all, many things
+in RabbitMQ are scoped per vhost.
+
+Both `switches/0` and `aliases/0` callbacks are optional.
+If your command doesn't have shorter argument names, you can omit `aliases/0`.
+If the command doesn't have any named arguments at all, you can omit both functions.
+
+We've described how the CLI should parse commands, now let's start describing what
+the command should do.
+
+### Command Banner
+
+We start with the `banner/2` function, that tells a user what the command is going to do.
+If you call the command with with `--dry-run` argument, it would only print the banner,
+without executing the actual command:
+
+```
+ def banner([qname], %{vhost: vhost,
+ if_empty: if_empty,
+ if_unused: if_unused}) do
+ if_empty_str = case if_empty do
+ true -> "if queue is empty"
+ false -> ""
+ end
+ if_unused_str = case if_unused do
+ true -> "if queue is unused"
+ false -> ""
+ end
+ "Deleting queue #{qname} on vhost #{vhost} " <>
+ Enum.join([if_empty_str, if_unused_str], " and ")
+ end
+
+```
+
+The function above can access arguments and command flags (named arguments)
+to decide what exactly it should do.
+
+### Default Argument Values and Argument Validation
+
+As you can see, the `banner/2` function accepts exactly one argument and expects
+the `vhost`, `if_empty` and `if_unused` options.
+To make sure the command have all the correct arguments, you can use
+the `merge_defaults/2` and `validate/2` functions:
+
+```
+ def merge_defaults(args, options) do
+ {
+ args,
+ Map.merge(%{if_empty: false, if_unused: false, vhost: "/"}, options)
+ }
+ end
+
+ def validate([], _options) do
+ {:validation_failure, :not_enough_args}
+ end
+ def validate([_,_|_], _options) do
+ {:validation_failure, :too_many_args}
+ end
+ def validate([""], _options) do
+ {
+ :validation_failure,
+ {:bad_argument, "queue name cannot be empty string."}
+ }
+ end
+ def validate([_], _options) do
+ :ok
+ end
+```
+
+The `merge_defaults/2` function accepts positional and options and returns a tuple
+with effective arguments and options that will be passed on to `validate/2`,
+`banner/2` and `run/2`.
+
+The `validate/2` function can return either `:ok` (just the atom) or a
+tuple in the form of `{:validation_failure, error}`. The function above checks
+that we have exactly one position argument and that it is not empty.
+
+While this is not enforced, for a command to be practical
+at least one `validate/2` head must return `:ok`.
+
+
+### Command Execution
+
+`validate/2` is useful for command line argument validation but there can be
+other things that require validation before a command can be executed. For example,
+a command may require a RabbitMQ node to be running (or stopped), a file to exist
+and be readable, an environment variable to be exported and so on.
+
+There's another validation function, `validate_execution_environment/2`, for
+such cases. That function accepts the same arguments and must return either `:ok`
+or `{:validation_failure, error}`. What's the difference, you may ask?
+`validate_execution_environment/2` is optional.
+
+To perform the actual command operation, the `run/2` command needs to be defined:
+
+```
+ def run([qname], %{node: node, vhost: vhost,
+ if_empty: if_empty, if_unused: if_unused}) do
+ ## Generate the queue resource name from queue name and vhost
+ queue_resource = :rabbit_misc.r(vhost, :queue, qname)
+ ## Lookup the queue on broker node using resource name
+ case :rabbit_misc.rpc_call(node, :rabbit_amqqueue, :lookup,
+ [queue_resource]) do
+ {:ok, queue} ->
+ ## Delete the queue
+ :rabbit_misc.rpc_call(node, :rabbit_amqqueue, :delete,
+ [queue, if_empty, if_unused]);
+ {:error, _} = error -> error
+ end
+ end
+```
+
+In the example above we delegate to a `:rabbit_misc` function in `run/2`. You can use any functions
+from [rabbit_common](https://github.com/rabbitmq/rabbitmq-common) directly but to
+do something on a broker (remote) node, you need to use RPC calls.
+It can be the standard Erlang `rpc:call` set of functions or `rabbit_misc:rpc_call/4`.
+The latter is used by all standard commands and is generally recommended.
+
+Target RabbitMQ node name is passed in as the `node` option, which is
+a global option and is available to all commands.
+
+
+### Command Output
+
+Finally we would like to present the user with a command execution result.
+To do that, we'll define `output/2` to format the `run/2` return value:
+
+```
+ def output({:error, :not_found}, _options) do
+ {:error, RabbitMQ.CLI.Core.ExitCodes.exit_usage, "Queue not found"}
+ end
+ def output({:error, :not_empty}, _options) do
+ {:error, RabbitMQ.CLI.Core.ExitCodes.exit_usage, "Queue is not empty"}
+ end
+ def output({:error, :in_use}, _options) do
+ {:error, RabbitMQ.CLI.Core.ExitCodes.exit_usage, "Queue is in use"}
+ end
+ def output({:ok, queue_length}, _options) do
+ {:ok, "Queue was successfully deleted with #{queue_length} messages"}
+ end
+ ## Use default output for all other cases
+ use RabbitMQ.CLI.DefaultOutput
+```
+
+We have function clauses for every possible output of `rabbit_amqqueue:delete/3` used
+in the `run/2` function.
+
+For a run to be successful, the `output/2` function should return a pair of `{:ok, result}`,
+and to indicate an error it should return a `{:error, exit_code, message}` tuple.
+`exit_code` must be an integer and `message` is a string or a list of strings.
+
+CLI program will exit with an `exit_code` in case of an error, or `0` in case of a success.
+
+`RabbitMQ.CLI.DefaultOutput` is a module which can handle common error cases
+(e.g. `badrpc` when the target RabbitMQ node cannot be contacted or authenticated with using the Erlang cookie).
+
+In the example above, we use Elixir's `use` statement to import
+function clauses for `output/2` from the `DefaultOutput` module. For
+some commands such delegation will be sufficient.
+
+### Testing the Command
+
+That's it. Now you can add this command to your plugin, compile it, enable the plugin and run
+
+`rabbitmqctl delete_queue my_queue --vhost my_vhost`
+
+to delete a queue.
+
+
+## Full Module Example in Elixir
+
+Full module definition in Elixir:
+
+```
+defmodule RabbitMQ.CLI.Ctl.Commands.DeleteQueueCommand do
+ @behaviour RabbitMQ.CLI.CommandBehaviour
+
+ def switches(), do: [if_empty: :boolean, if_unused: :boolean]
+ def aliases(), do: [e: :if_empty, u: :is_unused]
+
+ def usage(), do: "delete_queue queue_name [--if_empty|-e] [--if_unused|-u]"
+
+ def banner([qname], %{vhost: vhost,
+ if_empty: if_empty,
+ if_unused: if_unused}) do
+ if_empty_str = case if_empty do
+ true -> "if queue is empty"
+ false -> ""
+ end
+ if_unused_str = case if_unused do
+ true -> "if queue is unused"
+ false -> ""
+ end
+ "Deleting queue #{qname} on vhost #{vhost} " <>
+ Enum.join([if_empty_str, if_unused_str], " and ")
+ end
+
+ def merge_defaults(args, options) do
+ {
+ args,
+ Map.merge(%{if_empty: false, if_unused: false, vhost: "/"}, options)
+ }
+ end
+
+ def validate([], _options) do
+ {:validation_failure, :not_enough_args}
+ end
+ def validate([_,_|_], _options) do
+ {:validation_failure, :too_many_args}
+ end
+ def validate([""], _options) do
+ {
+ :validation_failure,
+ {:bad_argument, "queue name cannot be empty string."}
+ }
+ end
+ def validate([_], _options) do
+ :ok
+ end
+
+ def run([qname], %{node: node, vhost: vhost,
+ if_empty: if_empty, if_unused: if_unused}) do
+ ## Generate queue resource name from queue name and vhost
+ queue_resource = :rabbit_misc.r(vhost, :queue, qname)
+ ## Lookup a queue on broker node using resource name
+ case :rabbit_misc.rpc_call(node, :rabbit_amqqueue, :lookup,
+ [queue_resource]) do
+ {:ok, queue} ->
+ ## Delete queue
+ :rabbit_misc.rpc_call(node, :rabbit_amqqueue, :delete,
+ [queue, if_unused, if_empty, "cli_user"]);
+ {:error, _} = error -> error
+ end
+ end
+
+ def output({:error, :not_found}, _options) do
+ {:error, RabbitMQ.CLI.Core.ExitCodes.exit_usage, "Queue not found"}
+ end
+ def output({:error, :not_empty}, _options) do
+ {:error, RabbitMQ.CLI.Core.ExitCodes.exit_usage, "Queue is not empty"}
+ end
+ def output({:error, :in_use}, _options) do
+ {:error, RabbitMQ.CLI.Core.ExitCodes.exit_usage, "Queue is in use"}
+ end
+ def output({:ok, qlen}, _options) do
+ {:ok, "Queue was successfully deleted with #{qlen} messages"}
+ end
+ ## Use default output for all non-special case outputs
+ use RabbitMQ.CLI.DefaultOutput
+end
+```
+
+## Full Module Example in Erlang
+
+The same module implemented in Erlang. Note the fairly
+unusual Elixir module and behaviour names: since they contain
+dots, they must be escaped with single quotes to be valid Erlang atoms:
+
+```
+-module('Elixir.RabbitMQ.CLI.Ctl.Commands.DeleteQueueCommand').
+
+-behaviour('Elixir.RabbitMQ.CLI.CommandBehaviour').
+
+-export([switches/0, aliases/0, usage/0,
+ banner/2, merge_defaults/2, validate/2, run/2, output/2]).
+
+switches() -> [{if_empty, boolean}, {if_unused, boolean}].
+aliases() -> [{e, if_empty}, {u, is_unused}].
+
+usage() -> <<"delete_queue queue_name [--if_empty|-e] [--if_unused|-u] [--vhost|-p vhost]">>.
+
+banner([Qname], #{vhost := Vhost,
+ if_empty := IfEmpty,
+ if_unused := IfUnused}) ->
+ IfEmptyStr = case IfEmpty of
+ true -> ["if queue is empty"];
+ false -> []
+ end,
+ IfUnusedStr = case IfUnused of
+ true -> ["if queue is unused"];
+ false -> []
+ end,
+ iolist_to_binary(
+ io_lib:format("Deleting queue ~s on vhost ~s ~s",
+ [Qname, Vhost,
+ string:join(IfEmptyStr ++ IfUnusedStr, " and ")])).
+
+merge_defaults(Args, Options) ->
+ {
+ Args,
+ maps:merge(#{if_empty => false, if_unused => false, vhost => <<"/">>},
+ Options)
+ }.
+
+validate([], _Options) ->
+ {validation_failure, not_enough_args};
+validate([_,_|_], _Options) ->
+ {validation_failure, too_many_args};
+validate([<<"">>], _Options) ->
+ {
+ validation_failure,
+ {bad_argument, <<"queue name cannot be empty string.">>}
+ };
+validate([_], _Options) -> ok.
+
+run([Qname], #{node := Node, vhost := Vhost,
+ if_empty := IfEmpty, if_unused := IfUnused}) ->
+ %% Generate queue resource name from queue name and vhost
+ QueueResource = rabbit_misc:r(Vhost, queue, Qname),
+ %% Lookup a queue on broker node using resource name
+ case rabbit_misc:rpc_call(Node, rabbit_amqqueue, lookup, [QueueResource]) of
+ {ok, Queue} ->
+ %% Delete queue
+ rabbit_misc:rpc_call(Node, rabbit_amqqueue, delete,
+ [Queue, IfUnused, IfEmpty, <<"cli_user">>]);
+ {error, _} = Error -> Error
+ end.
+
+output({error, not_found}, _Options) ->
+ {
+ error,
+ 'Elixir.RabbitMQ.CLI.Core.ExitCodes':exit_usage(),
+ <<"Queue not found">>
+ };
+output({error, not_empty}, _Options) ->
+ {
+ error,
+ 'Elixir.RabbitMQ.CLI.Core.ExitCodes':exit_usage(),
+ <<"Queue is not empty">>
+ };
+output({error, in_use}, _Options) ->
+ {
+ error,
+ 'Elixir.RabbitMQ.CLI.Core.ExitCodes':exit_usage(),
+ <<"Queue is in use">>
+ };
+output({ok, qlen}, _Options) ->
+ {ok, <<"Queue was successfully deleted with #{qlen} messages">>};
+output(Other, Options) ->
+ 'Elixir.RabbitMQ.CLI.DefaultOutput':output(Other, Options, ?MODULE).
+```
+
+## Wrapping Up
+
+Phew. That's it! Implementing a new CLI command wasn't too difficult.
+That's because extensibility was one of the goals of this new CLI tool suite.
+
+
+## Feedback and Getting Help
+
+If you have any feedback about CLI tools extensibility,
+don't hesitate to reach out on the [RabbitMQ mailing list](https://groups.google.com/forum/#!forum/rabbitmq-users).
+