rabbitmq-server/deps/rabbitmq_cli/COMMAND_TUTORIAL.md

447 lines
15 KiB
Markdown
Raw Normal View History

2016-11-30 19:16:46 +08:00
# Implementing Your Own rabbitmqctl Command
2016-11-21 23:01:16 +08:00
2016-11-30 19:16:46 +08:00
## Introduction
2016-11-21 23:01:16 +08:00
2016-11-30 19:16:46 +08:00
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.
2016-11-21 23:01:16 +08:00
2016-11-30 19:16:46 +08:00
The CLI is written in the [Elixir programming
language](http://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.
2016-11-21 23:01:16 +08:00
To develop a new plugin you should check existing tutorials:
2016-11-30 19:16:46 +08:00
* [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](http://elixir-lang.org/getting-started/typespecs-and-behaviours.html).
It should fulfill certain requirements in order to be discovered and load by CLI tools:
2016-11-21 23:01:16 +08:00
2016-11-30 19:16:46 +08:00
* 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`
2016-11-21 23:01:16 +08:00
2016-11-30 19:16:46 +08:00
## Implementing `RabbitMQ.CLI.CommandBehaviour` in Erlang
2016-11-21 23:01:16 +08:00
2016-11-24 18:25:59 +08:00
When implementing a command in Erlang, you should add `Elixir` as a prefix to
2016-11-30 19:16:46 +08:00
the module name and behaviour, because CLI is written in Elixir.
2016-11-21 23:01:16 +08:00
It should match `Elixir.RabbitMQ.CLI.(.*).Commands.(.*)Command`
And implement `Elixir.RabbitMQ.CLI.CommandBehaviour`
2016-11-30 19:16:46 +08:00
## The Actual Tutorial
2016-11-24 18:25:59 +08:00
Let's write a command, that does something simple, e.g. deleting a queue.
2016-11-30 19:16:46 +08:00
We will use Elixir for that.
2016-11-21 23:01:16 +08:00
2016-11-30 19:16:46 +08:00
First we need to declare a module with a behaviour, for example:
2016-11-21 23:01:16 +08:00
```
defmodule RabbitMQ.CLI.Ctl.Commands.DeleteQueueCommand do
@behaviour RabbitMQ.CLI.CommandBehaviour
end
```
2016-11-30 19:16:46 +08:00
So far so good. But if we try to compile it, we'd see compilation errors:
2016-11-21 23:01:16 +08:00
```
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
```
2016-11-30 19:16:46 +08:00
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:
2016-11-21 23:01:16 +08:00
```
2016-11-24 18:25:59 +08:00
def usage(), do: "delete_queue queue_name [--if-empty|-e] [--if-unused|-u] [--vhost|-p vhost]"
2016-11-21 23:01:16 +08:00
```
2016-11-30 19:16:46 +08:00
### CLI Argument Parsing: Switches, Positional Arguments, Aliases
2016-11-21 23:01:16 +08:00
2016-11-30 19:16:46 +08:00
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.
2016-11-21 23:01:16 +08:00
2016-11-30 19:16:46 +08:00
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:
2016-11-21 23:01:16 +08:00
```
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.
2016-11-30 19:16:46 +08:00
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.
2016-11-30 19:16:46 +08:00
Both `switches/0` and `aliases/0` callbacks are optional.
2016-11-24 18:25:59 +08:00
If your command doesn't have shorter argument names, you can omit `aliases/0`.
2016-11-30 19:16:46 +08:00
If the command doesn't have any named arguments at all, you can omit both functions.
2016-11-24 18:25:59 +08:00
We've described how the CLI should parse commands, now let's start describing what
2016-11-21 23:01:16 +08:00
the command should do.
2016-11-30 19:16:46 +08:00
### Command Banner
2016-11-24 18:25:59 +08:00
We start with the `banner/2` function, that tells a user what the command is going to do.
2016-11-30 19:16:46 +08:00
If you call the command with with `--dry-run` argument, it would only print the banner,
without executing the actual command:
2016-11-21 23:01:16 +08:00
```
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
```
2016-11-30 19:16:46 +08:00
The function above can access arguments and command flags (named arguments)
to decide what exactly it should do.
2016-11-30 19:40:37 +08:00
### Default Argument Values and Validation
2016-11-21 23:01:16 +08:00
As you can see, the `banner/2` function accepts exactly one argument and expects
2016-11-30 19:16:46 +08:00
the `vhost`, `if_empty` and `if_unused` options.
To make sure the command have all the correct arguments, you can use
2016-11-30 19:40:37 +08:00
the `merge_defaults/2` and `validate/2` functions:
2016-11-21 23:01:16 +08:00
```
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
```
2016-11-30 19:40:37 +08:00
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`,
2016-11-21 23:01:16 +08:00
`banner/2` and `run/2`.
2016-11-30 19:40:37 +08:00
The `validate/2` function can return either `:ok` (just the atom) or a
tuple in the form of `{:validate, error}`. The function above checks
that we have exactly one position argument and that it is not empty.
2016-11-21 23:01:16 +08:00
2016-11-30 19:40:37 +08:00
While this is not enforced, for a command to be practical
at least one `validate/2` head must return `:ok`.
2016-11-21 23:01:16 +08:00
2016-11-30 19:40:37 +08:00
### Command Execution
To perform the actual command operation, the `run/2` command needs to be defined:
2016-11-21 23:01:16 +08:00
```
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
```
2016-11-30 19:40:37 +08:00
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 passsed in as the `node` option, which is
a global option and is available to all commands.
2016-11-21 23:01:16 +08:00
2016-11-30 19:40:37 +08:00
### Command Output
2016-11-21 23:01:16 +08:00
2016-11-30 19:40:37 +08:00
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:
2016-11-21 23:01:16 +08:00
```
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
2016-11-30 19:40:37 +08:00
## Use default output for all other cases
2016-11-21 23:01:16 +08:00
use RabbitMQ.CLI.DefaultOutput
```
2016-11-30 19:40:37 +08:00
We have function clauses for every possible output of `rabbit_amqqueue:delete/3` used
in the `run/2` function.
For a run to be successul, 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.
2016-11-21 23:01:16 +08:00
2016-11-30 19:40:37 +08:00
CLI program will exit with an `exit_code` in case of an error, or `0` in case of a success.
2016-11-21 23:01:16 +08:00
2016-11-30 19:40:37 +08:00
`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).
2016-11-21 23:01:16 +08:00
2016-11-30 19:40:37 +08:00
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.
2016-11-21 23:01:16 +08:00
2016-11-30 19:40:37 +08:00
### Testing the Command
That's it. Now you can add this command to your plugin, compile it, enable the plugin and run
2016-11-21 23:01:16 +08:00
`rabbitmqctl delete_queue my_queue --vhost my_vhost`
2016-11-30 19:40:37 +08:00
to delete a queue.
2016-11-21 23:01:16 +08:00
2016-11-30 19:40:37 +08:00
## Full Module Example in Elixir
2016-11-21 23:01:16 +08:00
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 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_empty, if_unused]);
{: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
```
2016-11-30 19:40:37 +08:00
## 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:
2016-11-21 23:01:16 +08:00
```
-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, IfEmpty, IfUnused]);
{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).
2016-11-30 19:16:46 +08:00
```
2016-11-30 19:40:37 +08:00
## 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).