3604df
From 322ca565ff22481a1406a205d8f6f44359b68009 Mon Sep 17 00:00:00 2001
3604df
From: Aravinda VK <avishwan@redhat.com>
3604df
Date: Tue, 31 May 2016 13:39:05 +0530
3604df
Subject: [PATCH 54/86] extras/cliutils: Utils for creating CLI tools for Gluster
3604df
3604df
Refer README.md for documentation.
3604df
3604df
This patch also includes changes from #14923 since without that
3604df
build will fail.
3604df
3604df
> Reviewed-on: http://review.gluster.org/14627
3604df
> Reviewed-on: http://review.gluster.org/14923
3604df
> Reviewed-by: Prashanth Pai <ppai@redhat.com>
3604df
> Reviewed-by: Niels de Vos <ndevos@redhat.com>
3604df
> Smoke: Gluster Build System <jenkins@build.gluster.org>
3604df
> NetBSD-regression: NetBSD Build System <jenkins@build.gluster.org>
3604df
> CentOS-regression: Gluster Build System <jenkins@build.gluster.org>
3604df
> Reviewed-by: Jeff Darcy <jdarcy@redhat.com>
3604df
3604df
BUG: 1351589
3604df
Change-Id: Ic88504177137136bbb4b8b2c304ecc4af9bcfe30
3604df
Signed-off-by: Aravinda VK <avishwan@redhat.com>
3604df
Reviewed-on: https://code.engineering.redhat.com/gerrit/84799
3604df
Reviewed-by: Milind Changire <mchangir@redhat.com>
3604df
Reviewed-by: Atin Mukherjee <amukherj@redhat.com>
3604df
---
3604df
 configure.ac                |    1 +
3604df
 extras/Makefile.am          |    2 +-
3604df
 extras/cliutils/Makefile.am |    4 +
3604df
 extras/cliutils/README.md   |  233 +++++++++++++++++++++++++++++++++++++++++++
3604df
 extras/cliutils/__init__.py |   29 ++++++
3604df
 extras/cliutils/cliutils.py |  212 +++++++++++++++++++++++++++++++++++++++
3604df
 glusterfs.spec.in           |    4 +
3604df
 7 files changed, 484 insertions(+), 1 deletions(-)
3604df
 create mode 100644 extras/cliutils/Makefile.am
3604df
 create mode 100644 extras/cliutils/README.md
3604df
 create mode 100644 extras/cliutils/__init__.py
3604df
 create mode 100644 extras/cliutils/cliutils.py
3604df
3604df
diff --git a/configure.ac b/configure.ac
3604df
index d84398d..75ebcd4 100644
3604df
--- a/configure.ac
3604df
+++ b/configure.ac
3604df
@@ -194,6 +194,7 @@ AC_CONFIG_FILES([Makefile
3604df
                 doc/Makefile
3604df
                 extras/Makefile
3604df
                 extras/glusterd.vol
3604df
+                extras/cliutils/Makefile
3604df
                 extras/init.d/Makefile
3604df
                 extras/init.d/glusterd.plist
3604df
                 extras/init.d/glusterd-Debian
3604df
diff --git a/extras/Makefile.am b/extras/Makefile.am
3604df
index 609d497..37b297f 100644
3604df
--- a/extras/Makefile.am
3604df
+++ b/extras/Makefile.am
3604df
@@ -5,7 +5,7 @@ EditorModedir = $(docdir)
3604df
 EditorMode_DATA = glusterfs-mode.el glusterfs.vim
3604df
 
3604df
 SUBDIRS = init.d systemd benchmarking hook-scripts $(OCF_SUBDIR) LinuxRPM \
3604df
-          $(GEOREP_EXTRAS_SUBDIR) ganesha snap_scheduler firewalld
3604df
+          $(GEOREP_EXTRAS_SUBDIR) ganesha snap_scheduler firewalld cliutils
3604df
 
3604df
 confdir = $(sysconfdir)/glusterfs
3604df
 conf_DATA = glusterfs-logrotate gluster-rsyslog-7.2.conf gluster-rsyslog-5.8.conf \
3604df
diff --git a/extras/cliutils/Makefile.am b/extras/cliutils/Makefile.am
3604df
new file mode 100644
3604df
index 0000000..7039703
3604df
--- /dev/null
3604df
+++ b/extras/cliutils/Makefile.am
3604df
@@ -0,0 +1,4 @@
3604df
+EXTRA_DIST= cliutils.py __init__.py
3604df
+
3604df
+cliutilsdir = @BUILD_PYTHON_SITE_PACKAGES@/gluster/cliutils
3604df
+cliutils_PYTHON = cliutils.py __init__.py
3604df
diff --git a/extras/cliutils/README.md b/extras/cliutils/README.md
3604df
new file mode 100644
3604df
index 0000000..ccb6080
3604df
--- /dev/null
3604df
+++ b/extras/cliutils/README.md
3604df
@@ -0,0 +1,233 @@
3604df
+# CLI utility for creating Cluster aware CLI tools for Gluster
3604df
+cliutils is a Python library which provides wrapper around `gluster system::
3604df
+execute` command to extend the functionalities of Gluster.
3604df
+
3604df
+Example use cases:
3604df
+- Start a service in all peer nodes of Cluster
3604df
+- Collect the status of a service from all peer nodes
3604df
+- Collect the config values from each peer nodes and display latest
3604df
+  config based on version.
3604df
+- Copy a file present in GLUSTERD_WORKDIR from one peer node to all
3604df
+  other peer nodes.(Geo-replication create push-pem is using this to
3604df
+  distribute the SSH public keys from all master nodes to all slave
3604df
+  nodes)
3604df
+- Generate pem keys in all peer nodes and collect all the public keys
3604df
+  to one place(Geo-replication gsec_create is doing this)
3604df
+- Provide Config sync CLIs for new features like `gluster-eventsapi`,
3604df
+  `gluster-restapi`, `gluster-mountbroker` etc.
3604df
+
3604df
+## Introduction
3604df
+
3604df
+If a executable file present in `$GLUSTER_LIBEXEC` directory in all
3604df
+peer nodes(Filename startswith `peer_`) then it can be executed by
3604df
+running `gluster system:: execute` command from any one peer node.
3604df
+
3604df
+- This command will not copy any executables to peer nodes, Script
3604df
+  should exist in all peer nodes to use this infrastructure. Raises
3604df
+  error in case script not exists in any one of the peer node.
3604df
+- Filename should start with `peer_` and should exist in
3604df
+  `$GLUSTER_LIBEXEC` directory.
3604df
+- This command can not be called from outside the cluster.
3604df
+
3604df
+To understand the functionality, create a executable file `peer_hello`
3604df
+under $GLUSTER_LIBEXEC directory and copy to all peer nodes.
3604df
+
3604df
+    #!/usr/bin/env bash
3604df
+    echo "Hello from $(gluster system:: uuid get)"
3604df
+
3604df
+Now run the following command from any one gluster node,
3604df
+
3604df
+    gluster system:: execute hello
3604df
+
3604df
+**Note:** Gluster will not copy the executable script to all nodes,
3604df
+  copy `peer_hello` script to all peer nodes to use `gluster system::
3604df
+  execute` infrastructure.
3604df
+
3604df
+It will run `peer_hello` executable in all peer nodes and shows the
3604df
+output from each node(Below example shows output from my two nodes
3604df
+cluster)
3604df
+
3604df
+    Hello from UUID: e7a3c5c8-e7ad-47ad-aa9c-c13907c4da84
3604df
+    Hello from UUID: c680fc0a-01f9-4c93-a062-df91cc02e40f
3604df
+
3604df
+## cliutils
3604df
+A Python wrapper around `gluster system:: execute` command is created
3604df
+to address the following issues
3604df
+
3604df
+- If a node is down in the cluster, `system:: execute` just skips it
3604df
+  and runs only in up nodes.
3604df
+- `system:: execute` commands are not user friendly
3604df
+- It captures only stdout, so handling errors is tricky.
3604df
+
3604df
+**Advantages of cliutils:**
3604df
+
3604df
+- Single executable file will act as node component as well as User CLI.
3604df
+- `execute_in_peers` utility function will merge the `gluster system::
3604df
+  execute` output with `gluster peer status` to identify offline nodes.
3604df
+- Easy CLI Arguments handling.
3604df
+- If node component returns non zero return value then, `gluster
3604df
+  system:: execute` will fail to aggregate the output from other
3604df
+  nodes. `node_output_ok` or `node_output_notok` utility functions
3604df
+  returns zero both in case of success or error, but returns json
3604df
+  with ok: true or ok:false respectively.
3604df
+- Easy to iterate on the node outputs.
3604df
+- Better error handling - Geo-rep CLIs `gluster system:: execute
3604df
+  mountbroker`, `gluster system:: execute gsec_create` and `gluster
3604df
+  system:: add_secret_pub` are suffering from error handling. These
3604df
+  tools are not notifying user if any failures during execute or if a node
3604df
+  is down during execute.
3604df
+
3604df
+### Hello World
3604df
+Create a file in `$LIBEXEC/glusterfs/peer_message.py` with following
3604df
+content.
3604df
+
3604df
+    #!/usr/bin/env python
3604df
+    from gluster.cliutils import Cmd, runcli, execute_in_peers, node_output_ok
3604df
+
3604df
+    class NodeHello(Cmd):
3604df
+        name = "node-hello"
3604df
+
3604df
+        def run(self, args):
3604df
+            node_output_ok("Hello")
3604df
+
3604df
+    class Hello(Cmd):
3604df
+        name = "hello"
3604df
+
3604df
+        def run(self, args):
3604df
+            out = execute_in_peers("node-hello")
3604df
+            for row in out:
3604df
+                print ("{0} from {1}".format(row.output, row.hostname))
3604df
+
3604df
+    if __name__ == "__main__":
3604df
+        runcli()
3604df
+
3604df
+When we run `python peer_message.py`, it will have two subcommands,
3604df
+"node-hello" and "hello". This file should be copied to
3604df
+`$LIBEXEC/glusterfs` directory in all peer nodes. User will call
3604df
+subcommand "hello" from any one peer node, which internally call
3604df
+`gluster system:: execute message.py node-hello`(This runs in all peer
3604df
+nodes and collect the outputs)
3604df
+
3604df
+For node component do not print the output directly, use
3604df
+`node_output_ok` or `node_output_notok` functions. `node_output_ok`
3604df
+additionally collects the node UUID and prints in JSON
3604df
+format. `execute_in_peers` function will collect this output and
3604df
+merges with `peers list` so that we don't miss the node information if
3604df
+that node is offline.
3604df
+
3604df
+If you observed already, function `args` is optional, if you don't
3604df
+have arguments then no need to create a function. When we run the
3604df
+file, we will have two subcommands. For example,
3604df
+
3604df
+    python peer_message.py hello
3604df
+    python peer_message.py node-hello
3604df
+
3604df
+First subcommand calls second subcommand in all peer nodes. Basically
3604df
+`execute_in_peers(NAME, ARGS)` will be converted into
3604df
+
3604df
+    CMD_NAME = FILENAME without "peers_"
3604df
+    gluster system:: execute <CMD_NAME> <SUBCOMMAND> <ARGS>
3604df
+
3604df
+In our example,
3604df
+
3604df
+    filename = "peer_message.py"
3604df
+    cmd_name = "message.py"
3604df
+    gluster system:: execute ${cmd_name} node-hello
3604df
+
3604df
+Now create symlink in `/usr/bin` or `/usr/sbin` directory depending on
3604df
+the usecase.(Optional step for usability)
3604df
+
3604df
+    ln -s /usr/libexec/glusterfs/peer_message.py /usr/bin/gluster-message
3604df
+
3604df
+Now users can use `gluster-message` instead of calling
3604df
+`/usr/libexec/glusterfs/peer_message.py`
3604df
+
3604df
+    gluster-message hello
3604df
+
3604df
+### Showing CLI output as Table
3604df
+
3604df
+Following example uses prettytable library, which can be installed
3604df
+using `pip install prettytable` or `dnf install python-prettytable`
3604df
+
3604df
+    #!/usr/bin/env python
3604df
+    from prettytable import PrettyTable
3604df
+    from gluster.cliutils import Cmd, runcli, execute_in_peers, node_output_ok
3604df
+
3604df
+    class NodeHello(Cmd):
3604df
+        name = "node-hello"
3604df
+
3604df
+        def run(self, args):
3604df
+            node_output_ok("Hello")
3604df
+
3604df
+    class Hello(Cmd):
3604df
+        name = "hello"
3604df
+
3604df
+        def run(self, args):
3604df
+            out = execute_in_peers("node-hello")
3604df
+            # Initialize the CLI table
3604df
+            table = PrettyTable(["ID", "NODE", "NODE STATUS", "MESSAGE"])
3604df
+            table.align["NODE STATUS"] = "r"
3604df
+            for row in out:
3604df
+                table.add_row([row.nodeid,
3604df
+                               row.hostname,
3604df
+                               "UP" if row.node_up else "DOWN",
3604df
+                               row.output if row.ok else row.error])
3604df
+
3604df
+            print table
3604df
+
3604df
+    if __name__ == "__main__":
3604df
+        runcli()
3604df
+
3604df
+
3604df
+Example output,
3604df
+
3604df
+    +--------------------------------------+-----------+-------------+---------+
3604df
+    |                  ID                  |    NODE   | NODE STATUS | MESSAGE |
3604df
+    +--------------------------------------+-----------+-------------+---------+
3604df
+    | e7a3c5c8-e7ad-47ad-aa9c-c13907c4da84 | localhost |          UP |  Hello  |
3604df
+    | bb57a4c4-86eb-4af5-865d-932148c2759b | vm2       |          UP |  Hello  |
3604df
+    | f69b918f-1ffa-4fe5-b554-ee10f051294e | vm3       |        DOWN |  N/A    |
3604df
+    +--------------------------------------+-----------+-------------+---------+
3604df
+
3604df
+## How to package in Gluster
3604df
+If the project is created in `$GLUSTER_SRC/tools/message`
3604df
+
3604df
+Add "message" to SUBDIRS list in `$GLUSTER_SRC/tools/Makefile.am`
3604df
+
3604df
+and then create a `Makefile.am` in `$GLUSTER_SRC/tools/message`
3604df
+directory with following content.
3604df
+
3604df
+    EXTRA_DIST = peer_message.py
3604df
+
3604df
+    peertoolsdir = $(libexecdir)/glusterfs/
3604df
+    peertools_SCRIPTS = peer_message.py
3604df
+
3604df
+    install-exec-hook:
3604df
+        $(mkdir_p) $(DESTDIR)$(bindir)
3604df
+        rm -f $(DESTDIR)$(bindir)/gluster-message
3604df
+        ln -s $(libexecdir)/glusterfs/peer_message.py \
3604df
+            $(DESTDIR)$(bindir)/gluster-message
3604df
+
3604df
+    uninstall-hook:
3604df
+        rm -f $(DESTDIR)$(bindir)/gluster-message
3604df
+
3604df
+Thats all. Add following files in `glusterfs.spec.in` if packaging is
3604df
+required.(Under `%files` section)
3604df
+
3604df
+    %{_libexecdir}/glusterfs/peer_message.py*
3604df
+    %{_bindir}/gluster-message
3604df
+
3604df
+## Who is using cliutils
3604df
+- gluster-mountbroker   http://review.gluster.org/14544
3604df
+- gluster-eventsapi     http://review.gluster.org/14248
3604df
+- gluster-georep-sshkey http://review.gluster.org/14732
3604df
+- gluster-restapi       https://github.com/aravindavk/glusterfs-restapi
3604df
+
3604df
+## Limitations/TODOs
3604df
+- Not yet possible to create CLI without any subcommand, For example
3604df
+  `gluster-message` without any arguments
3604df
+- Hiding node subcommands in `--help`(`gluster-message --help` will
3604df
+  show all subcommands including node subcommands)
3604df
+- Only positional arguments supported for node arguments, Optional
3604df
+  arguments can be used for other commands.
3604df
+- API documentation
3604df
diff --git a/extras/cliutils/__init__.py b/extras/cliutils/__init__.py
3604df
new file mode 100644
3604df
index 0000000..4bb8395
3604df
--- /dev/null
3604df
+++ b/extras/cliutils/__init__.py
3604df
@@ -0,0 +1,29 @@
3604df
+# -*- coding: utf-8 -*-
3604df
+# Reexporting the utility funcs and classes
3604df
+from cliutils import (runcli,
3604df
+                      sync_file_to_peers,
3604df
+                      execute_in_peers,
3604df
+                      execute,
3604df
+                      node_output_ok,
3604df
+                      node_output_notok,
3604df
+                      output_error,
3604df
+                      oknotok,
3604df
+                      yesno,
3604df
+                      get_node_uuid,
3604df
+                      Cmd,
3604df
+                      GlusterCmdException)
3604df
+
3604df
+
3604df
+# This will be useful when `from cliutils import *`
3604df
+__all__ = ["runcli",
3604df
+           "sync_file_to_peers",
3604df
+           "execute_in_peers",
3604df
+           "execute",
3604df
+           "node_output_ok",
3604df
+           "node_output_notok",
3604df
+           "output_error",
3604df
+           "oknotok",
3604df
+           "yesno",
3604df
+           "get_node_uuid",
3604df
+           "Cmd",
3604df
+           "GlusterCmdException"]
3604df
diff --git a/extras/cliutils/cliutils.py b/extras/cliutils/cliutils.py
3604df
new file mode 100644
3604df
index 0000000..4e035d7
3604df
--- /dev/null
3604df
+++ b/extras/cliutils/cliutils.py
3604df
@@ -0,0 +1,212 @@
3604df
+# -*- coding: utf-8 -*-
3604df
+from __future__ import print_function
3604df
+from argparse import ArgumentParser, RawDescriptionHelpFormatter
3604df
+import inspect
3604df
+import subprocess
3604df
+import os
3604df
+import xml.etree.cElementTree as etree
3604df
+import json
3604df
+import sys
3604df
+
3604df
+MY_UUID = None
3604df
+parser = ArgumentParser(formatter_class=RawDescriptionHelpFormatter,
3604df
+                        description=__doc__)
3604df
+subparsers = parser.add_subparsers(dest="mode")
3604df
+
3604df
+subcommands = {}
3604df
+cache_data = {}
3604df
+ParseError = etree.ParseError if hasattr(etree, 'ParseError') else SyntaxError
3604df
+
3604df
+
3604df
+class GlusterCmdException(Exception):
3604df
+    pass
3604df
+
3604df
+
3604df
+def get_node_uuid():
3604df
+    # Caches the Node UUID in global variable,
3604df
+    # Executes gluster system:: uuid get command only if
3604df
+    # calling this function for first time
3604df
+    global MY_UUID
3604df
+    if MY_UUID is not None:
3604df
+        return MY_UUID
3604df
+
3604df
+    cmd = ["gluster", "system::", "uuid", "get", "--xml"]
3604df
+    rc, out, err = execute(cmd)
3604df
+
3604df
+    if rc != 0:
3604df
+        return None
3604df
+
3604df
+    tree = etree.fromstring(out)
3604df
+    uuid_el = tree.find("uuidGenerate/uuid")
3604df
+    MY_UUID = uuid_el.text
3604df
+    return MY_UUID
3604df
+
3604df
+
3604df
+def yesno(flag):
3604df
+    return "Yes" if flag else "No"
3604df
+
3604df
+
3604df
+def oknotok(flag):
3604df
+    return "OK" if flag else "NOT OK"
3604df
+
3604df
+
3604df
+def output_error(message):
3604df
+    print (message, file=sys.stderr)
3604df
+    sys.exit(1)
3604df
+
3604df
+
3604df
+def node_output_ok(message=""):
3604df
+    # Prints Success JSON output and exits with returncode zero
3604df
+    out = {"ok": True, "nodeid": get_node_uuid(), "output": message}
3604df
+    print (json.dumps(out))
3604df
+    sys.exit(0)
3604df
+
3604df
+
3604df
+def node_output_notok(message):
3604df
+    # Prints Error JSON output and exits with returncode zero
3604df
+    out = {"ok": False, "nodeid": get_node_uuid(), "error": message}
3604df
+    print (json.dumps(out))
3604df
+    sys.exit(0)
3604df
+
3604df
+
3604df
+def execute(cmd):
3604df
+    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
3604df
+    out, err = p.communicate()
3604df
+    return p.returncode, out, err
3604df
+
3604df
+
3604df
+def get_pool_list():
3604df
+    cmd = ["gluster", "--mode=script", "pool", "list", "--xml"]
3604df
+    rc, out, err = execute(cmd)
3604df
+    if rc != 0:
3604df
+        output_error("Failed to get Pool Info: {0}".format(err))
3604df
+
3604df
+    tree = etree.fromstring(out)
3604df
+
3604df
+    pool = []
3604df
+    try:
3604df
+        for p in tree.findall('peerStatus/peer'):
3604df
+            pool.append({"nodeid": p.find("uuid").text,
3604df
+                         "hostname": p.find("hostname").text,
3604df
+                         "connected": (True if p.find("connected").text == "1"
3604df
+                                       else False)})
3604df
+    except (ParseError, AttributeError, ValueError) as e:
3604df
+        output_error("Failed to parse Pool Info: {0}".format(e))
3604df
+
3604df
+    return pool
3604df
+
3604df
+
3604df
+class NodeOutput(object):
3604df
+    def __init__(self, **kwargs):
3604df
+        self.nodeid = kwargs.get("nodeid", "")
3604df
+        self.hostname = kwargs.get("hostname", "")
3604df
+        self.node_up = kwargs.get("node_up", False)
3604df
+        self.ok = kwargs.get("ok", False)
3604df
+        self.output = kwargs.get("output", "N/A")
3604df
+        self.error = kwargs.get("error", "N/A")
3604df
+
3604df
+
3604df
+def execute_in_peers(name, args=[]):
3604df
+    # Get the file name of Caller function, If the file name is peer_example.py
3604df
+    # then Gluster peer command will be gluster system:: execute example.py
3604df
+    # Command name is without peer_
3604df
+    frame = inspect.stack()[1]
3604df
+    module = inspect.getmodule(frame[0])
3604df
+    actual_file = module.__file__
3604df
+    # If file is symlink then find actual file
3604df
+    if os.path.islink(actual_file):
3604df
+        actual_file = os.readlink(actual_file)
3604df
+
3604df
+    # Get the name of file without peer_
3604df
+    cmd_name = os.path.basename(actual_file).replace("peer_", "")
3604df
+    cmd = ["gluster", "system::", "execute", cmd_name, name] + args
3604df
+    rc, out, err = execute(cmd)
3604df
+    if rc != 0:
3604df
+        raise GlusterCmdException((rc, out, err, " ".join(cmd)))
3604df
+
3604df
+    out = out.strip().splitlines()
3604df
+
3604df
+    # JSON decode each line and construct one object with node id as key
3604df
+    all_nodes_data = {}
3604df
+    for node_data in out:
3604df
+        data = json.loads(node_data)
3604df
+        all_nodes_data[data["nodeid"]] = {
3604df
+            "nodeid": data.get("nodeid"),
3604df
+            "ok": data.get("ok"),
3604df
+            "output": data.get("output", ""),
3604df
+            "error": data.get("error", "")}
3604df
+
3604df
+    # gluster pool list
3604df
+    pool_list = get_pool_list()
3604df
+
3604df
+    data_out = []
3604df
+    # Iterate pool_list and merge all_nodes_data collected above
3604df
+    # If a peer node is down then set node_up = False
3604df
+    for p in pool_list:
3604df
+        p_data = all_nodes_data.get(p.get("nodeid"), None)
3604df
+        row_data = NodeOutput(node_up=False,
3604df
+                              hostname=p.get("hostname"),
3604df
+                              nodeid=p.get("nodeid"),
3604df
+                              ok=False)
3604df
+
3604df
+        if p_data is not None:
3604df
+            # Node is UP
3604df
+            row_data.node_up = True
3604df
+            row_data.ok = p_data.get("ok")
3604df
+            row_data.output = p_data.get("output")
3604df
+            row_data.error = p_data.get("error")
3604df
+
3604df
+        data_out.append(row_data)
3604df
+
3604df
+    return data_out
3604df
+
3604df
+
3604df
+def sync_file_to_peers(fname):
3604df
+    # Copy file from current node to all peer nodes, fname
3604df
+    # is path after GLUSTERD_WORKDIR
3604df
+    cmd = ["gluster", "system::", "copy", "file", fname]
3604df
+    rc, out, err = execute(cmd)
3604df
+    if rc != 0:
3604df
+        raise GlusterCmdException((rc, out, err))
3604df
+
3604df
+
3604df
+class Cmd(object):
3604df
+    name = ""
3604df
+
3604df
+    def run(self, args):
3604df
+        # Must required method. Raise NotImplementedError if derived class
3604df
+        # not implemented this method
3604df
+        raise NotImplementedError("\"run(self, args)\" method is "
3604df
+                                  "not implemented by \"{0}\"".format(
3604df
+                                      self.__class__.__name__))
3604df
+
3604df
+
3604df
+def runcli():
3604df
+    # Get list of Classes derived from class "Cmd" and create
3604df
+    # a subcommand as specified in the Class name. Call the args
3604df
+    # method by passing subcommand parser, Derived class can add
3604df
+    # arguments to the subcommand parser.
3604df
+    for c in Cmd.__subclasses__():
3604df
+        cls = c()
3604df
+        if getattr(cls, "name", "") == "":
3604df
+            raise NotImplementedError("\"name\" is not added "
3604df
+                                      "to \"{0}\"".format(
3604df
+                                          cls.__class__.__name__))
3604df
+
3604df
+        p = subparsers.add_parser(cls.name)
3604df
+        args_func = getattr(cls, "args", None)
3604df
+        if args_func is not None:
3604df
+            args_func(p)
3604df
+
3604df
+        # A dict to save subcommands, key is name of the subcommand
3604df
+        subcommands[cls.name] = cls
3604df
+
3604df
+    # Get all parsed arguments
3604df
+    args = parser.parse_args()
3604df
+
3604df
+    # Get the subcommand to execute
3604df
+    cls = subcommands.get(args.mode, None)
3604df
+
3604df
+    # Run
3604df
+    if cls is not None:
3604df
+        cls.run(args)
3604df
diff --git a/glusterfs.spec.in b/glusterfs.spec.in
3604df
index 06f1de1..a60f216 100644
3604df
--- a/glusterfs.spec.in
3604df
+++ b/glusterfs.spec.in
3604df
@@ -1249,6 +1249,7 @@ exit 0
3604df
 # introducing glusterfs module in site packages.
3604df
 # so that all other gluster submodules can reside in the same namespace.
3604df
 %{python_sitelib}/gluster/__init__.*
3604df
+%{python_sitelib}/gluster/cliutils
3604df
 
3604df
 %if ( 0%{!?_without_rdma:1} )
3604df
 %files rdma
3604df
@@ -2008,6 +2009,9 @@ end
3604df
 
3604df
 %changelog
3604df
 * Fri Sep 16 2016 Aravinda VK <avishwan@redhat.com>
3604df
+- Added Python subpackage "cliutils" under gluster (#1342356)
3604df
+
3604df
+* Fri Sep 16 2016 Aravinda VK <avishwan@redhat.com>
3604df
 - Changed attribute of eventsconfig.json file as same as other configs (#1375532)
3604df
 
3604df
 * Fri Sep 16 2016 Aravinda VK <avishwan@redhat.com>
3604df
-- 
3604df
1.7.1
3604df