Rework CLI, add list-domains
diff --git a/reclass_tools/__init__.py b/reclass_tools/__init__.py
index 0418604..e69de29 100644
--- a/reclass_tools/__init__.py
+++ b/reclass_tools/__init__.py
@@ -1,38 +0,0 @@
-import os
-import time
-import logging.config
-
-
-LOGGER_SETTINGS = {
-    'version': 1,
-    'disable_existing_loggers': False,
-    'loggers': {
-        'reclass_tools': {
-            'level': 'DEBUG',
-            'handlers': ['console_output'],
-        },
-        'paramiko': {'level': 'WARNING'},
-        'iso8601': {'level': 'WARNING'},
-        'keystoneauth': {'level': 'WARNING'},
-    },
-    'handlers': {
-        'console_output': {
-            'class': 'logging.StreamHandler',
-            'level': 'INFO',
-            'formatter': 'default',
-            'stream': 'ext://sys.stdout',
-        },
-    },
-    'formatters': {
-        'default': {
-            'format': '%(asctime)s - %(levelname)s - %(filename)s:'
-                      '%(lineno)d -- %(message)s',
-            'datefmt': '%Y-%m-%d %H:%M:%S',
-        },
-    },
-}
-
-logging.config.dictConfig(LOGGER_SETTINGS)
-# set logging timezone to GMT
-logging.Formatter.converter = time.gmtime
-logger = logging.getLogger(__name__)
diff --git a/reclass_tools/cli.py b/reclass_tools/cli.py
index becff4e..c696400 100644
--- a/reclass_tools/cli.py
+++ b/reclass_tools/cli.py
@@ -1,4 +1,4 @@
-#    Copyright 2013 - 2016 Mirantis, Inc.
+#    Copyright 2013 - 2017 Mirantis, Inc.
 #
 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
 #    not use this file except in compliance with the License. You may obtain
@@ -22,157 +22,206 @@
 from reclass_tools import walk_models
 
 
-def execute(params):
+class Shell(object):
+    def __init__(self, args):
+        self.args = args
+        self.params = self.get_params()
 
-    results = walk_models.get_all_reclass_params(
-        params.paths,
-        verbose=params.verbose)
+    def execute(self):
+        command_name = 'do_{}'.format(self.params.command.replace('-', '_'))
+        command_method = getattr(self, command_name)
+        command_method()
 
-    print(yaml.dump(results))
+    def do_get_key(self):
+        results = walk_models.remove_reclass_parameter(
+            self.params.path,
+            self.params.key_name,
+            verbose=self.params.verbose,
+            pretend=True)
+
+    def do_del_key(self):
+        results = walk_models.remove_reclass_parameter(
+            self.params.path,
+            self.params.key_name,
+            verbose=self.params.verbose,
+            pretend=False)
+
+    def do_list_params(self):
+        results = walk_models.get_all_reclass_params(
+            self.params.path,
+            verbose=self.params.verbose)
+        print(yaml.dump(results))
+
+    def do_list_domains(self):
+        try:
+            from reclass_tools import reclass_models
+        except ImportError:
+            sys.exit("Please run this tool on the salt-master node "
+                     "with installed 'reclass'")
+        inventory = reclass_models.inventory_list()
+        reclass_storage = reclass_models.reclass_storage(inventory=inventory)
+        print('\n'.join(sorted(reclass_storage.keys())))
 
 
-def dump_params(args=None):
+    def do_list_nodes(self):
+        try:
+            from reclass_tools import reclass_models
+        except ImportError:
+            sys.exit("Please run this tool on the salt-master node "
+                     "with installed 'reclass'")
+
+        inventory = reclass_models.inventory_list(domain=self.params.domain)
+        vcp_nodes = reclass_models.vcp_list(domain=self.params.domain,
+                                            inventory=inventory)
+        vcp_node_names = ['{0}.{1}'.format(name, domain)
+                          for name, domain in vcp_nodes]
+
+        if self.params.vcp_only:
+            print('\n'.join(sorted(vcp_node_names)))
+        elif self.params.non_vcp_only:
+            print('\n'.join(sorted((node_name for node_name in inventory.keys()
+                                    if node_name not in vcp_node_names))))
+        else:
+            print('\n'.join(sorted(inventory.keys())))
+
+    def do_show_context(self):
+        try:
+            from reclass_tools import create_inventory
+        except ImportError:
+            sys.exit("Please run this tool on the salt-master node "
+                     "with installed 'reclass'")
+
+        current_underlay_context = create_inventory.create_inventory_context(
+            domain=self.params.domain, keys=self.params.keys)
+
+        print(yaml.dump(current_underlay_context, default_flow_style=False))
+
+    def do_render(self):
+        try:
+            from reclass_tools import create_inventory
+        except ImportError:
+            sys.exit("Please run this tool on the salt-master node "
+                     "with installed 'reclass'")
+
+        if not self.params.template_dir or not self.params.output_dir \
+                or not self.params.contexts:
+            sys.exit("Missing parameters, see: reclass-tools render -h")
+
+        create_inventory.render_dir(template_dir=self.params.template_dir,
+                                    output_dir=self.params.output_dir,
+                                    contexts=self.params.contexts)
+
+    def get_params(self):
+
+        verbose_parser = argparse.ArgumentParser(add_help=False)
+        verbose_parser.add_argument('--verbose', dest='verbose',
+                                    action='store_const', const=True,
+                                    help='Show verbosed output', default=False)
+
+        key_parser = argparse.ArgumentParser(add_help=False)
+        key_parser_help = (
+                'Key name to find in reclass model files, for example:'
+                ' parameters.linux.network.interface')
+        key_parser.add_argument('key_name', help=key_parser_help, default=None)
+
+        keys_parser = argparse.ArgumentParser(add_help=False)
+        keys_parser.add_argument(
+            'keys',
+            help='Key names to find in reclass model files', nargs='*')
+
+        path_parser = argparse.ArgumentParser(add_help=False)
+        path_parser.add_argument(
+            'path',
+            help='Path to search for *.yml files.', nargs='+')
+
+        domain_parser = argparse.ArgumentParser(add_help=False)
+        domain_parser.add_argument(
+            '--domain', '-d', dest='domain',
+            help=('Show only the nodes which names are ended with the '
+                  'specified domain, for example: example.local'))
+
+        vcp_only_parser = argparse.ArgumentParser(add_help=False)
+        vcp_only_parser.add_argument(
+            '--vcp-only', dest='vcp_only',
+            action='store_const', const=True,
+            help=('Show only VCP nodes (present in '
+                  'parameters.salt.control.cluster.internal.node)'),
+            default=False)
+
+        non_vcp_only_parser = argparse.ArgumentParser(add_help=False)
+        non_vcp_only_parser.add_argument(
+            '--non-vcp-only', dest='non_vcp_only',
+            action='store_const', const=True,  default=False,
+            help=('Show only non-VCP nodes (absent in '
+                  'parameters.salt.control.cluster.internal.node)'))
+
+        render_parser = argparse.ArgumentParser(add_help=False)
+        render_parser.add_argument(
+            '--template-dir', '-t', dest='template_dir',
+            help=('Coockiecutter-based template directory'))
+        render_parser.add_argument(
+            '--output-dir', '-o', dest='output_dir',
+            help=('Path to the directory where the rendered '
+                  'template will be placed'))
+        render_parser.add_argument(
+            '--context', '-c', dest='contexts', nargs='+',
+            help=('YAML/JSON files with context data to render '
+                  'the template'))
+
+
+
+        parser = argparse.ArgumentParser(
+            description="Manage virtual environments. "
+                        "For additional help, use with -h/--help option")
+        subparsers = parser.add_subparsers(title="Operation commands",
+                                           help='available commands',
+                                           dest='command')
+
+        # TODO: add-class NNN [to] MMM.yml # can be used with 'render'
+        subparsers.add_parser('get-key',
+                              parents=[key_parser, path_parser,
+                                       verbose_parser],
+                              help="Find a key in YAMLs found in <path>",
+                              description=("Get a key collected from "
+                                           "different YAMLs"))
+        subparsers.add_parser('del-key',
+                              parents=[key_parser, path_parser,
+                                       verbose_parser],
+                              help="Delete a key from YAMLs found in <path>",
+                              description="Delete a key from different YAMLs")
+        subparsers.add_parser('list-params',
+                              parents=[path_parser, verbose_parser],
+                              help=("Collect all options for "
+                                    "'parameters._params' keys from YAMLs "
+                                    "found in <path>"))
+        subparsers.add_parser('list-nodes',
+                              parents=[domain_parser, vcp_only_parser,
+                                       non_vcp_only_parser],
+                              help=("List nodes that are available for "
+                                    "reclass. Use on salt-master node only!"))
+        subparsers.add_parser('list-domains',
+                              help=("List domains that are available from "
+                                    "reclass models. Use on salt-master "
+                                    "node only!"))
+        subparsers.add_parser('show-context',
+                              parents=[domain_parser, keys_parser],
+                              help=("Show domain nodes with rendered content "
+                                    "for specified keys. Use on salt-master "
+                                    "node for already generated inventory "
+                                    "only!"))
+        subparsers.add_parser('render',
+                              parents=[render_parser],
+                              help=("Render cookiecutter template using "
+                                    "multiple metadata sources"))
+
+        if len(self.args) == 0:
+            self.args = ['-h']
+        return parser.parse_args(self.args)
+
+
+def main(args=None):
     if args is None:
         args = sys.argv[1:]
 
-    parser = argparse.ArgumentParser(
-        formatter_class=argparse.RawTextHelpFormatter,
-        description="")
-    parser.add_argument('--verbose', dest='verbose', action='store_const', const=True,
-                        help='Show verbosed output.', default=False)
-    parser.add_argument('paths', help='Paths to search for *.yml files.', nargs='+')
-
-    if len(args) == 0:
-        args = ['-h']
-
-    params = parser.parse_args(args)
-    results = walk_models.get_all_reclass_params(
-        params.paths,
-        verbose=params.verbose)
-
-    print(yaml.dump(results))
-
-
-def show_key(args=None):
-    remove_key(args=args, pretend=True)
-
-
-def remove_key(args=None, pretend=False):
-    if args is None:
-        args = sys.argv[1:]
-
-    key_parser = argparse.ArgumentParser(add_help=False)
-    if pretend:
-        key_parser_help = (
-            'Key name to find in reclass model files, for example:'
-            ' reclass-show-key parameters.linux.network.interface'
-            ' /path/to/model/')
-    else:
-        key_parser_help = (
-            'Key name to remove from reclass model files, for example:'
-            ' reclass-remove-key parameters.linux.network.interface'
-            ' /path/to/model/')
-    key_parser.add_argument('key_name', help=key_parser_help)
-
-    parser = argparse.ArgumentParser(parents=[key_parser],
-        formatter_class=argparse.RawTextHelpFormatter,
-        description="")
-    parser.add_argument('--verbose', dest='verbose', action='store_const', const=True,
-                        help='Show verbosed output.', default=False)
-    parser.add_argument('paths', help='Paths to search for *.yml files.', nargs='+')
-
-    if len(args) == 0:
-        args = ['-h']
-
-    params = parser.parse_args(args)
-    results = walk_models.remove_reclass_parameter(
-        params.paths,
-        params.key_name,
-        verbose=params.verbose,
-        pretend=pretend)
-
-
-def inventory_list(args=None):
-    try:
-        from reclass_tools import reclass_models
-    except ImportError:
-        print("Please run this tool on the salt-master node with installed 'reclass'")
-        return
-
-    parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter,
-                                     description="")
-    parser.add_argument('--domain', '-d', dest='domain',
-                        help=('Show only the nodes which names are ended with the specified domain, for example:'
-                              ' reclass-inventory-list -d example.local'))
-
-
-    params = parser.parse_args(args)
-
-    inventory = reclass_models.inventory_list(domain=params.domain)
-
-    print('\n'.join(sorted(inventory.keys())))
-
-def vcp_list(args=None):
-    try:
-        from reclass_tools import reclass_models
-    except ImportError:
-        print("Please run this tool on the salt-master node with installed 'reclass'")
-        return
-
-    parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter,
-                                     description="")
-    parser.add_argument('--domain', '-d', dest='domain',
-                        help=('Show only the nodes which names are ended with the specified domain, for example:'
-                              ' reclass-inventory-list -d example.local'))
-
-    params = parser.parse_args(args)
-
-    vcp_node_names = reclass_models.vcp_list(domain=params.domain)
-    #print('\n'.join(sorted(vcp_node_names)))
-    print('\n'.join(sorted(('{0}.{1}'.format(name, domain) for name, domain in vcp_node_names))))
-
-
-def create_inventory_context(args=None):
-    try:
-        from reclass_tools import create_inventory
-    except ImportError:
-        print("Please run this tool on the salt-master node with installed 'reclass'")
-        return
-
-    parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter,
-                                     description="Dumps nodes and specified node parameters from reclass, for example: create_inventory_context -d example.local parameters.linux.network.interface parameters.linux.storage")
-    parser.add_argument('--domain', '-d', dest='domain',
-                        help=('Show only the nodes which names are ended with the specified domain, for example:'
-                              ' reclass-inventory-list -d example.local'))
-    parser.add_argument('keys', help=(
-        'Reclass key names to dump with nodes'), nargs='*')
-
-    params = parser.parse_args(args)
-
-    current_underlay_context = create_inventory.create_inventory_context(domain=params.domain, keys=params.keys)
-
-    print(yaml.dump(current_underlay_context, default_flow_style=False))
-
-
-def render_dir(args=None):
-    try:
-        from reclass_tools import create_inventory
-    except ImportError:
-        print("Please run this tool on the salt-master node with installed 'reclass'")
-        return
-
-    parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter,
-                                     description="Render a coockiecutter-based template directory using several different context files")
-    parser.add_argument('--template-dir', '-t', dest='template_dir',
-                        help=('Coockiecutter-based template directory'))
-    parser.add_argument('--output-dir', '-o', dest='output_dir',
-                        help=('Path to the directory where the rendered template will be placed'))
-    parser.add_argument('--context', '-c', dest='contexts',
-                        help=('Path to the directory where the rendered template will be placed'),
-                        nargs='+')
-
-    params = parser.parse_args(args)
-
-    create_inventory.render_dir(template_dir=params.template_dir, output_dir=params.output_dir, contexts=params.contexts)
-
-
+    shell = Shell(args)
+    shell.execute()
diff --git a/reclass_tools/create_inventory.py b/reclass_tools/create_inventory.py
index ce0d7a6..9b03632 100644
--- a/reclass_tools/create_inventory.py
+++ b/reclass_tools/create_inventory.py
@@ -1,9 +1,6 @@
 import yaml
 import json
-
-from cookiecutter import __version__
-#from cookiecutter.log import configure_logger
-#from cookiecutter.main import cookiecutter
+import sys
 
 from cookiecutter import generate
 from cookiecutter.exceptions import UndefinedVariableInTemplate
@@ -41,12 +38,7 @@
     reclass_storage = reclass_models.reclass_storage(domain=domain, inventory=inventory)
 
     if domain is None:
-        raise Exception("Please specify a domain name from: \n{}".format('\n'.join(reclass_storage.keys())))
-
-    #current_underlay_context = {
-    #    'current_clusters': {
-    #    }
-    #}
+        sys.exit("Error: please specify a domain name from: \n{}".format('\n'.join(reclass_storage.keys())))
 
     for storage_domain, storage_nodes in reclass_storage.items():
         if storage_domain != domain:
@@ -78,9 +70,6 @@
                     if reclass_key:
                         helpers.create_nested_key(current_cluster_nodes[inventory_node_name], path=key_path, value=reclass_key)
 
-        #current_underlay_context['current_clusters'][domain] = {
-        #    'nodes': current_cluster_nodes
-        #}
         current_underlay_context = {
             'cookiecutter': {
                 'cluster_name': storage_domain,
@@ -91,32 +80,6 @@
     return current_underlay_context
 
 
-    #1. Generate jinga interfaces / hw details based on node information provided to jinja
-
-    #2. Generate appropriate includes to reclass.storate model in config node
-    #configure_logger(
-    #    stream_level='DEBUG' if verbose else 'INFO',
-    #    debug_file=debug_file,
-    #)
-
-#current_clusters:
-#  <cluster_names>:
-#    nodes:
-#      <node_names>:
-#        name: ctl01
-#        reclass_storage_name: openstack_control_node01
-#        # if classes - then classes
-#        roles:
-#        - vcp  # to select wich interface type to use
-#        #- openstack_controller  # Don't forget to map the roles to corresponded classes if needed
-#        parameters: # there is just a DUMP of the existing model,
-#                    # which could be re-used complete or particulary for rendering new model
-#          linux:
-#            network:
-#              interfaces:
-#                ..
-
-
 def render_dir(template_dir, output_dir, contexts):
     """Coockiecutter echancement to use several source JSON files
 
@@ -128,23 +91,7 @@
                           dict is in the same order as files in the list.
     """
 
-#ipdb> repo_dir
-#u'/root/cookiecutter-templates/cluster_product/openstack'
-#ipdb> context
-#{u'cookiecutter': {u'openstack_telemetry_node02_hostname': u'mdb02', ... }}
-#ipdb> overwrite_if_exists
-#False
-#ipdb> output_dir
-#'/root/my_new_deployment/'
-
-    print(template_dir)
-    print(output_dir)
-    print(contexts)
-    #return
-    #repo_dir = '/root/cookiecutter-templates/cluster_product/openstack'
     overwrite_if_exists = True
-    #output_dir = '/root/my_new_deployment/'
-    #context = {'cookiecutter': {'openstack_telemetry_node02_hostname': 'mdb02' }}
 
     merged_context = {}
     for fcon in contexts:
@@ -153,16 +100,10 @@
         elif fcon.endswith('.json'):
             context = helpers.json_read(fcon)
         else:
-            print("Error: Please use YAML or JSON files for contexts")
-            return # should be exit 1
+            sys.exit("Error: Please use YAML or JSON files for contexts")
 
-
-        #merged_context.update(context)
-        #merged_context = dict(chain(merged_context.items(), context.items()))
         merged_context = helpers.merge_nested_objects(merged_context, context)
 
-    #print(yaml.dump(merged_context, default_flow_style=False))
-
     try:
         generate.generate_files(
             repo_dir=template_dir,
@@ -171,14 +112,11 @@
             output_dir=output_dir
         )
 
-
     except UndefinedVariableInTemplate as undefined_err:
-        print('>>> {}'.format(undefined_err.message))
-        print('>>> Error message: {}'.format(undefined_err.error.message))
-
         context_str = yaml.dump(
             undefined_err.context,
             default_flow_style=False
         )
         print('='*15 + ' Context: '+ '='*15 + '\n{}'.format(context_str) + '='*40)
-        return
+        print('>>> {}'.format(undefined_err.message))
+        sys.exit('>>> Error message: {}'.format(undefined_err.error.message))
diff --git a/reclass_tools/walk_models.py b/reclass_tools/walk_models.py
index 7017722..8b4d91f 100644
--- a/reclass_tools/walk_models.py
+++ b/reclass_tools/walk_models.py
@@ -148,11 +148,11 @@
                     if nested_key:
                         found_keys[fyml.fname] = copy.deepcopy(nested_key)
                         if pretend:
-                            print("\nFound {0} in {1}".format('.'.join(remove_key),
+                            print("\n---\n# Found {0} in {1}".format('.'.join(remove_key),
                                                                    fyml.fname))
                             print(yaml.dump(nested_key, default_flow_style=False))
                         else:
-                            print("\nRemoving {0} from {1}".format('.'.join(remove_key),
+                            print("\n---\n# Removing {0} from {1}".format('.'.join(remove_key),
                                                                    fyml.fname))
                             print(yaml.dump(nested_key, default_flow_style=False))