# CLI utility for creating Cluster aware CLI tools for Gluster
cliutils is a Python library which provides wrapper around `gluster system::
execute` command to extend the functionalities of Gluster.

Example use cases:
- Start a service in all peer nodes of Cluster
- Collect the status of a service from all peer nodes
- Collect the config values from each peer nodes and display latest
  config based on version.
- Copy a file present in GLUSTERD_WORKDIR from one peer node to all
  other peer nodes.(Geo-replication create push-pem is using this to
  distribute the SSH public keys from all primary nodes to all secondary
  nodes)
- Generate pem keys in all peer nodes and collect all the public keys
  to one place(Geo-replication gsec_create is doing this)
- Provide Config sync CLIs for new features like `gluster-eventsapi`,
  `gluster-restapi`, `gluster-mountbroker` etc.

## Introduction

If a executable file present in `$GLUSTER_LIBEXEC` directory in all
peer nodes(Filename startswith `peer_`) then it can be executed by
running `gluster system:: execute` command from any one peer node.

- This command will not copy any executables to peer nodes, Script
  should exist in all peer nodes to use this infrastructure. Raises
  error in case script not exists in any one of the peer node.
- Filename should start with `peer_` and should exist in
  `$GLUSTER_LIBEXEC` directory.
- This command can not be called from outside the cluster.

To understand the functionality, create a executable file `peer_hello`
under $GLUSTER_LIBEXEC directory and copy to all peer nodes.

    #!/usr/bin/env bash
    echo "Hello from $(gluster system:: uuid get)"

Now run the following command from any one gluster node,

    gluster system:: execute hello

**Note:** Gluster will not copy the executable script to all nodes,
  copy `peer_hello` script to all peer nodes to use `gluster system::
  execute` infrastructure.

It will run `peer_hello` executable in all peer nodes and shows the
output from each node(Below example shows output from my two nodes
cluster)

    Hello from UUID: e7a3c5c8-e7ad-47ad-aa9c-c13907c4da84
    Hello from UUID: c680fc0a-01f9-4c93-a062-df91cc02e40f

## cliutils
A Python wrapper around `gluster system:: execute` command is created
to address the following issues

- If a node is down in the cluster, `system:: execute` just skips it
  and runs only in up nodes.
- `system:: execute` commands are not user friendly
- It captures only stdout, so handling errors is tricky.

**Advantages of cliutils:**

- Single executable file will act as node component as well as User CLI.
- `execute_in_peers` utility function will merge the `gluster system::
  execute` output with `gluster peer status` to identify offline nodes.
- Easy CLI Arguments handling.
- If node component returns non zero return value then, `gluster
  system:: execute` will fail to aggregate the output from other
  nodes. `node_output_ok` or `node_output_notok` utility functions
  returns zero both in case of success or error, but returns json
  with ok: true or ok:false respectively.
- Easy to iterate on the node outputs.
- Better error handling - Geo-rep CLIs `gluster system:: execute
  mountbroker`, `gluster system:: execute gsec_create` and `gluster
  system:: add_secret_pub` are suffering from error handling. These
  tools are not notifying user if any failures during execute or if a node
  is down during execute.

### Hello World
Create a file in `$LIBEXEC/glusterfs/peer_message.py` with following
content.

    #!/usr/bin/python3
    from gluster.cliutils import Cmd, runcli, execute_in_peers, node_output_ok

    class NodeHello(Cmd):
        name = "node-hello"

        def run(self, args):
            node_output_ok("Hello")

    class Hello(Cmd):
        name = "hello"

        def run(self, args):
            out = execute_in_peers("node-hello")
            for row in out:
                print ("{0} from {1}".format(row.output, row.hostname))

    if __name__ == "__main__":
        runcli()

When we run `python peer_message.py`, it will have two subcommands,
"node-hello" and "hello". This file should be copied to
`$LIBEXEC/glusterfs` directory in all peer nodes. User will call
subcommand "hello" from any one peer node, which internally call
`gluster system:: execute message.py node-hello`(This runs in all peer
nodes and collect the outputs)

For node component do not print the output directly, use
`node_output_ok` or `node_output_notok` functions. `node_output_ok`
additionally collects the node UUID and prints in JSON
format. `execute_in_peers` function will collect this output and
merges with `peers list` so that we don't miss the node information if
that node is offline.

If you observed already, function `args` is optional, if you don't
have arguments then no need to create a function. When we run the
file, we will have two subcommands. For example,

    python peer_message.py hello
    python peer_message.py node-hello

First subcommand calls second subcommand in all peer nodes. Basically
`execute_in_peers(NAME, ARGS)` will be converted into

    CMD_NAME = FILENAME without "peers_"
    gluster system:: execute <CMD_NAME> <SUBCOMMAND> <ARGS>

In our example,

    filename = "peer_message.py"
    cmd_name = "message.py"
    gluster system:: execute ${cmd_name} node-hello

Now create symlink in `/usr/bin` or `/usr/sbin` directory depending on
the usecase.(Optional step for usability)

    ln -s /usr/libexec/glusterfs/peer_message.py /usr/bin/gluster-message

Now users can use `gluster-message` instead of calling
`/usr/libexec/glusterfs/peer_message.py`

    gluster-message hello

### Showing CLI output as Table

Following example uses prettytable library, which can be installed
using `pip install prettytable` or `dnf install python-prettytable`

    #!/usr/bin/python3
    from prettytable import PrettyTable
    from gluster.cliutils import Cmd, runcli, execute_in_peers, node_output_ok

    class NodeHello(Cmd):
        name = "node-hello"

        def run(self, args):
            node_output_ok("Hello")

    class Hello(Cmd):
        name = "hello"

        def run(self, args):
            out = execute_in_peers("node-hello")
            # Initialize the CLI table
            table = PrettyTable(["ID", "NODE", "NODE STATUS", "MESSAGE"])
            table.align["NODE STATUS"] = "r"
            for row in out:
                table.add_row([row.nodeid,
                               row.hostname,
                               "UP" if row.node_up else "DOWN",
                               row.output if row.ok else row.error])

            print table

    if __name__ == "__main__":
        runcli()


Example output,

    +--------------------------------------+-----------+-------------+---------+
    |                  ID                  |    NODE   | NODE STATUS | MESSAGE |
    +--------------------------------------+-----------+-------------+---------+
    | e7a3c5c8-e7ad-47ad-aa9c-c13907c4da84 | localhost |          UP |  Hello  |
    | bb57a4c4-86eb-4af5-865d-932148c2759b | vm2       |          UP |  Hello  |
    | f69b918f-1ffa-4fe5-b554-ee10f051294e | vm3       |        DOWN |  N/A    |
    +--------------------------------------+-----------+-------------+---------+

## How to package in Gluster
If the project is created in `$GLUSTER_SRC/tools/message`

Add "message" to SUBDIRS list in `$GLUSTER_SRC/tools/Makefile.am`

and then create a `Makefile.am` in `$GLUSTER_SRC/tools/message`
directory with following content.

    EXTRA_DIST = peer_message.py

    peertoolsdir = $(libexecdir)/glusterfs/
    peertools_SCRIPTS = peer_message.py

    install-exec-hook:
        $(mkdir_p) $(DESTDIR)$(bindir)
        rm -f $(DESTDIR)$(bindir)/gluster-message
        ln -s $(libexecdir)/glusterfs/peer_message.py \
            $(DESTDIR)$(bindir)/gluster-message

    uninstall-hook:
        rm -f $(DESTDIR)$(bindir)/gluster-message

Thats all. Add following files in `glusterfs.spec.in` if packaging is
required.(Under `%files` section)

    %{_libexecdir}/glusterfs/peer_message.py*
    %{_bindir}/gluster-message

## Who is using cliutils
- gluster-mountbroker   http://review.gluster.org/14544
- gluster-eventsapi     http://review.gluster.org/14248
- gluster-georep-sshkey http://review.gluster.org/14732
- gluster-restapi       https://github.com/gluster/restapi

## Limitations/TODOs
- Not yet possible to create CLI without any subcommand, For example
  `gluster-message` without any arguments
- Hiding node subcommands in `--help`(`gluster-message --help` will
  show all subcommands including node subcommands)
- Only positional arguments supported for node arguments, Optional
  arguments can be used for other commands.
- API documentation
