#!/usr/bin/env python3
# -*- coding:utf-8 -*-
# vim: set ts=2 sw=2 sts=2 et:

# ansible-inventory. An Inventory Manager for Ansible
# Copyright (C) 2016  Diego Blanco <diego.blanco@treitos.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.


# ( IMPORTS
from __future__ import print_function
from re import fullmatch
import argparse
import cmd
import configparser
import errno
import fcntl
import json
import os
import readline
import signal
import sys
import time
import uuid
# )

# ( CONFIG
AI_HOME = os.path.join( os.environ['HOME'], '.ansible' )
CONFIG_PATH = os.path.join( AI_HOME, 'ansible-inventory.cfg' )
HISTORY_FILE = os.path.join( AI_HOME, 'ansible-inventory_history' )
BASE_CONFIG = """[global]
use_colors = True

# backend: redis, file
backend = file


[file_backend]
path = ~/.ansible/inventory.json

[redis_backend]
host =
port =
password =
"""
# )


# ( AUX

def check_requirements():
  if not os.path.exists( AI_HOME ):
    os.mkdir( AI_HOME )

  if not os.path.exists( CONFIG_PATH ):
    with open( CONFIG_PATH, 'w' ) as cf:
      cf.write( BASE_CONFIG )
      cf.close()

  if not os.path.exists( HISTORY_FILE ):
    open( HISTORY_FILE, 'a').close()
  readline.read_history_file( HISTORY_FILE )
  readline.set_completer_delims(' ')


def generate_colors():
  color_list = []
  for style in range( 3 ):
    for fg in range( 31, 38):
      color = ';'.join([str(style), str(fg)])
      color_list.append(color)
  return color_list


def generate_colors256():
  DARK_COLORS = list(range( 0, 8 )) + list(range( 16, 28 )) + list(range( 52, 76 )) + list(range( 232, 250 ))
  color_list = []
  for c_256_n in range( 1, 255 ):
    if c_256_n not in DARK_COLORS:
      # Grant all colors have the same string length
      if c_256_n < 10:
        c_256_s = '00%d' % c_256_n
      elif c_256_n < 100:
        c_256_s = '0%d' % c_256_n
      else:
        c_256_s = '%d' % c_256_n
      color = '38;5;' + c_256_s
      color_list.append( color )
  return color_list


def C(word, word_to_hash=None):
  if not USE_COLORS:
    return word
  if not word_to_hash:
    word_to_hash = word
  n=0
  for c in word_to_hash:
    n+=ord(c)
  n = n*word_to_hash.__len__()**2
  color = COLORS[ n % COLORS.__len__() ]
  return '\x1b[%sm%s\x1b[0m' % (color, word)


# SimpleFlock from https://github.com/derpston/python-simpleflock
class SimpleFlock:
   """Provides the simplest possible interface to flock-based file locking. Intended for use with the `with` syntax. It will create/truncate/delete the lock file as necessary."""

   def __init__(self, path, timeout = None):
      self._path = path
      self._timeout = timeout
      self._fd = None

   def __enter__(self):
      self._fd = os.open(self._path, os.O_CREAT)
      start_lock_search = time.time()
      while True:
         try:
            fcntl.flock(self._fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
            # Lock acquired!
            return
         except (OSError, IOError) as ex:
            if ex.errno != errno.EAGAIN:  # Resource temporarily unavailable
               raise
            elif self._timeout is not None and time.time() > (start_lock_search + self._timeout):
               # Exceeded the user-specified timeout.
               raise

         # TODO It would be nice to avoid an arbitrary sleep here, but spinning
         # without a delay is also undesirable.
         time.sleep(0.1)

   def __exit__(self, *args):
      fcntl.flock(self._fd, fcntl.LOCK_UN)
      os.close(self._fd)
      self._fd = None

      # Try to remove the lock file, but don't try too hard because it is
      # unnecessary. This is mostly to help the user see whether a lock
      # exists by examining the filesystem.
      try:
         os.unlink(self._path)
      except:
         pass
# )


# ( GLOBALS
VERSION = '0.3.2'
AUTHOR_NAME = 'Diego Blanco'
AUTHOR_MAIL = 'diego.blanco@treitos.com'
URL = 'https://github.com/diego-treitos/ansible-inventory'
USE_COLORS = True
COLORS = generate_colors256()

C_BASE = '\x1b[1;37m'
C_FAIL = '\x1b[1;31m'
C_GOOD = '\x1b[1;32m'
C_WARN = '\x1b[1;33m'
C_INFO = '\x1b[1;34m'
C_RESET = '\x1b[0;0m'
# )


# ( CLASSES
class file_backend:
  "Backend class for ansible-inventory that uses a json file for storage"

  def __init__( self, backend_parameters ):
    self.json_path = backend_parameters
    self.lockfile = '/tmp/.ansible-inventory_file_backend.lock'
    self.__lock = SimpleFlock('/tmp/.ansible-inventory_file_backend_main.lock', timeout=3)

  def load_inventory(self):
    "Returns a dictionary with the inventory contents as required by Inventory class"
    if os.path.exists( self.json_path ):
      with SimpleFlock( self.lockfile, timeout=3 ):
        with open( self.json_path ) as inv_file:
          return json.loads( inv_file.read() )
    else:
        return {}

  def save_inventory(self, inventory):
    "Saves the inventory from a dictionary with the inventory contents from the Inventory class"
    with SimpleFlock( self.lockfile, timeout=3 ):
      with open( self.json_path, 'w' ) as inv_file:
        inv_file.write( json.dumps( inventory ) )

  def lock(self):
    "Locks the backend for reading and writting"
    self.__lock.__enter__()

  def unlock(self):
    "Unlocks the backend"
    self.__lock.__exit__()


class redis_backend:
  "Backend class for ansible-inventory that uses redis for storage"

  def __init__( self, backend_parameters ):
    host = backend_parameters['host']
    port=6379
    password = None
    if 'port' in backend_parameters:
      port = backend_parameters['port']
    if 'password' in backend_parameters:
      password = backend_parameters['password']

    self.r = redis.Redis( host=host, port=port, password=password )
    self.i = 'ansible_inventory'
    self.uuid = str( uuid.uuid4() )
    self.__lock_name = 'ansible_inventory_redis_backend_lock'
    self.__timeout = 3  # seconds

  def load_inventory( self ):
    "Returns a dictionary with the inventory contents as required by Inventory class"
    i = self.r.get( self.i )
    if i:
      return json.loads( i.decode("utf-8") )
    else:
      return {}

  def save_inventory(self, inventory):
    "Saves the inventory from a dictionary with the inventory contents from the Inventory class"
    self.r.set( self.i, json.dumps( inventory ) )

  def lock(self):
    "Locks the backend for reading and writting"
    # Try to get the lock or raise exception on timeout
    t=0
    while not self.r.set( self.__lock_name, self.uuid, nx=True, px=self.__timeout*1000 ):
      if t >= self.__timeout:
        raise BlockingIOError
      time.sleep( 0.5 )
      t+=0.5

  def unlock(self):
    "Unlocks the backend"
    if self.r.get( self.__lock_name ) == self.uuid:
      self.r.delete( self.__lock_name )


class InventoryException( Exception ):

  def __init__(self, message, targets=()):
    super().__init__( message )
    if type( targets ) == str:
      self.targets = ( targets, )
    else:
      self.targets = tuple( targets )


class InventoryInfoException( InventoryException ):
  pass


class InventoryWarnException( InventoryException ):
  pass


class Inventory:

  # Inventory variable
  I = None

### Internal inventory format
#{
#          "all"   : { 'hosts':[ all hosts here ], 'children':[], 'vars',{} },
#    "databases"   : {
#            "hosts" : [ "host1.example.com", "host2.example.com" ],
#            "vars"  : {
#                  "a" : true
#            }
#    },
#    "webservers"  : [ "host2.example.com", "host3.example.com" ],
#    "atlanta"     : {
#          "hosts"   : [ "host1.example.com", "host4.example.com", "host5.example.com" ],
#          "vars"    : {
#                "b"   : false
#          },
#          "children": [ "marietta", "5points" ]
#    },
#    "marietta"    : [ "host6.example.com" ],
#    "5points"     : [ "host7.example.com" ]
#       "_meta"    : {
#         "hostvars" : {
#           "moocow.example.com"     : { "asdf" : 1234 },
#           "llama.example.com"      : { "asdf" : 5678 },
#           "xeira"                  : { "ansible_host" : xeira.example.com },
#         }
#    }
#}
  def write(f):
    "Decorator for functions that change the inventory. This should grant inventory integrity when several concurrent ansible-inventory sessions"
    def wrapper(s, *args, **kwargs):
      try:
        s.backend.lock()
        s.reload()
        r = f(s, *args, **kwargs)
        s.save()
        return r
      except BlockingIOError:
        raise InventoryException("Backend temporally unavailable. Please try again.")
      except InventoryException:
        raise
      finally:
        s.backend.unlock()

    return wrapper

  def read(f):
    "Decorator for functions that read, so they use they have the most updated information in case of several concurrent ansible-inventory sessions"
    def wrapper(s, *kargs, **kwargs):
      try:
        s.reload()
        return f(s, *kargs, **kwargs)
      except Exception as e:
        raise InventoryException( e.__str__() )
    return wrapper

  def __init__(self, backend):
    self.backend = backend
    self.reload()

  def __ensure_inventory_skel(self):
    'Ensures the basic structure of the inventory is pressent'
    if '_meta' not in self.I:
      self.I['_meta'] = {}
    if 'all' not in self.I:
      self.I['all'] = {
        'children': [],
        'hosts': [],
        'vars': {}
      }
    if 'hostvars' not in self.I['_meta']:
      self.I['_meta']['hostvars'] = {}

  def save(self):
    "Saves the inventory to persistence backend"
    self.backend.save_inventory( self.I )

  def reload(self):
    "Loads the inventory from the persistence backend"
    self.I = self.backend.load_inventory()
    self.__ensure_inventory_skel()

  def __list_hosts(self, h_regex='.*'):
    'Internal: Returns a list of known hosts in the inventory. If regex specified only matching hosts will be returned.'
    hosts = []
    for g in self.I:
      if g == '_meta':
        for h in self.I['_meta']['hostvars']:
          if h not in hosts and fullmatch( h_regex, h ):
            hosts.append( h )
      else:
        for h in self.__get_group_hosts( g ):
          if h not in hosts and fullmatch( h_regex, h ):
            hosts.append( h )
    return hosts

  def __list_groups(self, g_regex='.*'):
    'Internal: Returns a list of available groups. If g_regex is specified, only matching groups will be returned'
    groups = []
    for g in self.I:
      if g == '_meta':
        continue
      if fullmatch( g_regex, g ):
        groups.append(g)
    return groups

  def __get_group_hosts(self, group):
    'Internal: Returns a list of hosts in a group'
    if group in self.I:
      if isinstance( self.I[group], list):
        return self.I[group]
      if isinstance( self.I[group], dict ):
        return self.I[group]['hosts']
    else:
      return []

  def __get_host_groups(self, host):
    'Internal: Returns a list of groups where a host belongs'
    groups = []
    for g in self.I:
      if g == '_meta':
        continue
      if host in self.__get_group_hosts(g):
        groups.append(g)
    return groups

  def __get_group_children(self, group):
    'Internal: Returns the list of subgroups in a group'
    if group in self.I:
      if isinstance( self.I[group], dict ) and 'children' in self.I[group]:
        return self.I[group]['children']
    return []

  def __set_host_host(self, h_name, host):
    'Sets or replaces a host address for a host'
    if h_name in self.I['_meta']['hostvars']:
      self.I['_meta']['hostvars'][h_name]['ansible_host'] = host
    else:
      self.I['_meta']['hostvars'][h_name] = { 'ansible_host': host }

  def __set_host_port(self, h_name, port):
    'Sets or replaces a host address for a host'
    if h_name in self.I['_meta']['hostvars']:
      self.I['_meta']['hostvars'][h_name]['ansible_port'] = port
    else:
      self.I['_meta']['hostvars'][h_name] = { 'ansible_port': port }

  def local_list_vars( self, v_regex='.*' ):
    'Returns a list of variables in the inventory. If regex specified only matching variables will be returned. This only checks the local memory copy of the inventory.'
    i_vars = []
    for g in self.I:
      if g == '_meta':
        for h in self.I['_meta']['hostvars']:
          for v in self.I['_meta']['hostvars'][ h ]:
            if v not in i_vars and fullmatch( v_regex, v ):
              i_vars.append( v )
      else:
        for v in self.I[ g ]['vars']:
          if v not in i_vars and fullmatch( v_regex, v ):
            i_vars.append( v )
    return i_vars

  def local_list_hosts(self, h_regex='.*'):
    'Returns a list of known hosts in the inventory. If regex specified only matching hosts will be returned. This only checks the local memory copy of the inventory.'
    return self.__list_hosts( h_regex )

  def local_list_groups(self, g_regex='.*'):
    'Returns a list of available groups. If g_regex is specified, only matching groups will be returned. This only checks the local memory copy of the inventory.'
    return self.__list_groups( g_regex )

  @read
  def get_ansible_json(self):
    'Returns the ansible json'
    return json.dumps( self.I )

  @read
  def get_ansible_host_json(self, host):
    'Returns the ansible json for a host'
    if host in self.I['_meta']['hostvars']:
      return json.dumps( self.I['_meta']['hostvars'][ host ] )
    return json.dumps( {} )

  @read
  def list_hosts(self, h_regex='.*'):
    'Returns a list of known hosts in the inventory. If regex specified only matching hosts will be returned'
    return self.__list_hosts( h_regex )

  @read
  def list_groups(self, g_regex='.*'):
    'Returns a list of available groups. If g_regex is specified, only matching groups will be returned'
    return self.__list_groups( g_regex )

  @read
  def get_group_vars(self, group):
    'Returns a dict with the group vars'
    if group in self.I:
      if isinstance( self.I[group], dict):
        if 'vars' in self.I[group]:
          return self.I[group]['vars']
    return {}

  @read
  def get_group_hosts(self, group):
    'Returns a list of hosts in a group'
    return self.__get_group_hosts( group )

  @read
  def get_group_children(self, group):
    'Returns the list of subgroups in a group'
    return self.__get_group_children( group )

  @read
  def get_group_parent(self, group):
    'Returns the name of a group parent or None if no parent is found'
    for g in self.I:
      if isinstance( self.I[g], dict ) and 'children' in self.I[g] and group in self.I[g]['children']:
        return g
    return None

  @read
  def get_host_vars(self, host):
    'Returns a dict with the host vars'
    if host in self.I['_meta']['hostvars']:
      return self.I['_meta']['hostvars'][host]
    else:
      return {}

  @read
  def get_host_groups(self, host):
    'Returns a list of groups where a host belongs'
    return self.__get_host_groups( host )

  @write
  def add_hosts_to_groups(self, h_regex, g_regex):
    'Adds a hosts matching h_regex to groups matching g_regex'
    matching_hosts = self.__list_hosts( h_regex )
    if not matching_hosts:
      raise InventoryException('No host matches your selection')

    matching_groups = self.__list_groups( g_regex )
    if not matching_groups:
      raise InventoryException('No group matches your selection')

    for h_name in matching_hosts:
      for g_name in matching_groups:
        if isinstance( self.I[ g_name ], list ):
          if h_name not in self.I[ g_name ]:
            self.I[ g_name ].append( h_name )
        elif isinstance( self.I[ g_name ], dict ):
          if h_name not in self.I[ g_name ]['hosts']:
            self.I[ g_name ]['hosts'].append( h_name )

  @write
  def add_host(self, h_name, h_host=None, h_port=None ):
    'Adds a host'
    if h_name in self.__list_hosts():
      raise InventoryException('Host %s already exists', h_name)
    else:
      self.I['all']['hosts'].append( h_name )
      self.I['_meta']['hostvars'][ h_name ] = {}

    if h_host:
      self.__set_host_host( h_name, h_host )
    if h_port:
      self.__set_host_port( h_name, h_port )

  @write
  def add_group(self, group):
    'Adds a group'
    if group not in self.I:
      self.I[ group ] = {
        'hosts': [],
        'vars': {},
        'children': []
      }
    else:
      raise InventoryException('Group %s already exists', group)

  @write
  def add_group_to_groups(self, group, g_regex):
    'Adds a single group to groups matching g_regex'
    if group not in self.__list_groups():
      raise InventoryException('Group %s does not exist', targets=group)

    matching_groups = self.__list_groups( g_regex )
    if not matching_groups:
      raise InventoryException('No group matches your selection')

    for g in matching_groups:
      if isinstance( self.I[g], list):
        hosts = self.I[g]
        self.I[g] = {
          'hosts': hosts,
          'vars': {},
          'children': []
        }
      elif isinstance( self.I[g], dict) and 'children' not in self.I[g]:
        self.I[g]['children'] = []

      if group not in self.I[g]['children']:
        self.I[g]['children'].append( group )

  @write
  def add_var_to_groups(self, v_name, v_value, g_regex):
    'Adds a variable a to groups matching g_regex'
    matching_groups = self.__list_groups( g_regex )
    if not matching_groups:
      raise InventoryException('No group matches your selection')

    exist_in = []
    for g in matching_groups:
      if isinstance( self.I[g], list):
        hosts = self.I[g]
        self.I[g] = {
          'hosts': hosts,
          'vars': {},
          'children': []
        }
      elif isinstance( self.I[g], dict) and 'vars' not in self.I[g]:
        self.I[g]['vars'] = {}

      if v_name not in self.I[g]['vars']:
        self.I[g]['vars'][v_name] = v_value
      else:
        exist_in.append( g )

    if exist_in:
      str_list = '%s ' * exist_in.__len__()
      raise InventoryException('Var %s already exist in these groups: '+str_list, targets=(v_name,)+tuple( exist_in ) )

  @write
  def add_var_to_hosts(self, v_name, v_value, h_regex):
    'Adds a variable to hosts matching h_regex'
    matching_hosts = self.__list_hosts( h_regex )
    if not matching_hosts:
      raise InventoryException('No host matches your selection')

    exist_in = []
    for h in matching_hosts:
      if h not in self.I['_meta']['hostvars']:
        self.I['_meta']['hostvars'][h] = {v_name : v_value}
      elif v_name not in self.I['_meta']['hostvars'][h]:
        self.I['_meta']['hostvars'][h][v_name] = v_value
      else:
        exist_in.append( h )
    if exist_in:
      str_list = '%s ' * exist_in.__len__()
      raise InventoryException('Var %s already exist in these hosts: '+str_list, targets=(v_name,)+tuple( exist_in ) )

  @write
  def rename_host(self, h_name, new_name):
    'Renames a host'
    hosts = self.__list_hosts()
    if new_name in hosts:
      raise InventoryException('Host %s already exists', new_name)
    if h_name not in hosts:
      raise InventoryException('Host %s does not exist', h_name)

    if h_name in self.I['_meta']['hostvars']:
      hvars = self.I['_meta']['hostvars'].pop(h_name)
      self.I['_meta']['hostvars'][new_name] = hvars
    for g in self.__get_host_groups( h_name ):
      if isinstance( self.I[g], list ):
        self.I[g].remove( h_name )
        self.I[g].append( new_name )
      elif isinstance( self.I[g], dict):
        self.I[g]['hosts'].remove( h_name )
        self.I[g]['hosts'].append( new_name )

  @write
  def change_host(self, h_name, h_host=None, h_port=None):
    'Changes the host address or port of a host'
    if h_name not in self.__list_hosts():
      raise InventoryException('Host %s does not exist', h_name)
    if h_host:
      self.__set_host_host( h_name, h_host )
    if h_port:
      self.__set_host_port( h_name, h_port )

  @write
  def rename_host_var(self, v_name, new_name, h_regex):
    'Renames a variable in a set of hosts matching a regular expression'
    for h in self.__list_hosts():
      if fullmatch( h_regex, h ) and h in self.I['_meta']['hostvars'] and v_name in self.I['_meta']['hostvars'][h]:
        v_value = self.I['_meta']['hostvars'][h].pop(v_name)
        self.I['_meta']['hostvars'][h][new_name] = v_value

  @write
  def change_host_var(self, v_name, v_value, h_regex):
    'Changes the value of a variable in the hosts matching a regular expression in case it is defined'
    for h in self.__list_hosts():
      if fullmatch( h_regex, h ) and h in self.I['_meta']['hostvars'] and v_name in self.I['_meta']['hostvars'][h]:
        self.I['_meta']['hostvars'][h][v_name] = v_value

  @write
  def rename_group(self, g_name, new_name):
    'Renames a group'
    if new_name in self.I:
      raise InventoryException('Group %s already exists', new_name)
    if g_name not in self.I:
      raise InventoryException('Group %s does not exist', g_name)
    g_data = self.I.pop(g_name)
    self.I[new_name] = g_data

  @write
  def rename_group_var(self, v_name, new_name, g_regex):
    'Renames a variable in a set of groups matching a regular expression'
    for g in self.I:
      if g == '_meta':
        continue
      if fullmatch( g_regex, g ) and isinstance( self.I[g], dict) and 'vars' in self.I[g]:
        if v_name in self.I[g]['vars']:
          v_value = self.I[g]['vars'].pop(v_name)
          self.I[g]['vars'][new_name] = v_value

  @write
  def change_group_var(self, v_name, v_value, g_regex):
    'Changes the value of a variable in the groups matching a regular expression in case it is defined'
    for g in self.I:
      if g == '_meta':
        continue
      if fullmatch( g_regex, g ) and isinstance( self.I[g], dict) and 'vars' in self.I[g]:
        if v_name in self.I[g]['vars']:
          self.I[g]['vars'][v_name] = v_value

  @write
  def remove_host(self, h_name, from_groups=[]):
    'Removes the selected host. If from_groups is provided, the host will only removed from those groups.'
    if from_groups:
      groups = from_groups
    else:
      groups = self.__get_host_groups( h_name )
      if h_name in self.I['_meta']['hostvars']:
        self.I['_meta']['hostvars'].pop(h_name)
    for g in groups:
      g_hosts = self.__get_group_hosts( g )
      if g_hosts and h_name in g_hosts:
        g_hosts.remove( h_name )

  @write
  def remove_group(self, g_name, from_groups=[]):
    'Removes the selected group. If from_groups is provided, the group will only removed from those groups.'
    if from_groups:
      for g in from_groups:
        g_child = self.__get_group_children( g )
        if g_name in g_child:
          g_child.remove( g_name )
    else:
      if g_name == 'all':
        raise InventoryException('Group %s cannot be removed', 'all')
      for g in self.I:
        g_child = self.__get_group_children( g )
        if g_name in g_child:
          g_child.remove( g_name )
      if g_name in self.I:
        self.I.pop( g_name )
      else:
        raise InventoryException('Group %s does not exist.', g_name)

  @write
  def remove_host_var(self, v_name, h_name ):
    'Removes a variable from a host'
    if h_name in self.I['_meta']['hostvars'] and v_name in self.I['_meta']['hostvars'][h_name]:
      self.I['_meta']['hostvars'][h_name].pop( v_name )

  @write
  def remove_group_var(self, v_name, g_name):
    'Removes a variable from a group'
    if g_name in self.I and 'vars' in self.I[g_name] and v_name in self.I[g_name]['vars']:
      self.I[g_name]['vars'].pop( v_name )


class Console(cmd.Cmd):

  intro = """
Welcome to %s.

 Author: %s %s
Project: %s

Type 'help' for help.""" % ( C_BASE+'Ansible Inventory ' + VERSION,
                             C_RESET+AUTHOR_NAME, '<'+AUTHOR_MAIL+'>'+C_BASE,
                             C_RESET+URL )

  if USE_COLORS:
    # We use \001 and \002 to delimit non printable characters so the history completion is not messed up
    prompt = '\n\001'+C_BASE+"\002¬¬ \001"+C_RESET+'\002'
  else:
    prompt = '\n¬¬ '
  file = None

  allowed_commands = ['add', 'show', 'edit', 'del', 'help', 'exit' ]

  def console_handler( f ):
    def wrapper(s, arg ):
      # parse args
      args={ 0: f.__name__.split('do_')[1] }
      i=0
      for a in arg.split():
        i+=1
        if i>0 and '=' in a:
          args[i] = tuple(a.split('=', 1))
        else:
          args[i] = a

      # handle exceptions
      try:
        return f(s, args)
      except InventoryException as e:
        targets = tuple( C(x) for x in e.targets )
        s.__error( e.__str__() % targets )
      except InventoryWarnException as e:
        targets = tuple( C(x) for x in e.targets )
        s.__warn( e.__str__() % targets )
      except InventoryInfoException as e:
        targets = tuple( C(x) for x in e.targets )
        s.__info( e.__str__() % targets )
    wrapper.__doc__ = f.__doc__
    return wrapper

  def __init__(self, inventory):
    super(Console, self).__init__()
    self.inventory = inventory
    signal.signal(signal.SIGINT, self.__signal_sigint)

  def emptyline(self):
    "Override the default from cmd.Cmd that executes last command instead"
    pass

  def __close(self):
    "Close shell file"
    if self.file:
      self.file.close()
      self.file = None

  def __signal_sigint(self, signal, frame):
    "Catch sigint and exit cleanly"
    print('')
    self.do_exit(None)
    sys.exit(0)

  def __validate_args(self, args):
    "Validates the commands and args sent to the shell"

    # Get number of arguments
    n_args = args.keys().__len__()

    # Create initial structure to clasify positional and optional arguments
    args_dict = {
      'positional': {},
      'optional': {}
    }

    pos_i=0
    for i in range( 0, n_args ):
      if isinstance(args[i], tuple):
        args_dict['optional'][ args[i][0] ] = args[i][1]
      else:
        args_dict['positional'][ pos_i ] = args[i]
        pos_i+=1

    # Main command
    cmd = args_dict['positional'][0]

    # Check number of arguments
    if cmd == 'show':
      if n_args < 2:
        return 'Not enough arguments'
    else:
      if n_args < 3:
        return 'Not enough arguments'

    # Get subcommand (l2cmd) and check it
    if args_dict['positional'].__len__() < 2:
        return 'No subcommand provided'
    l2cmd = args_dict['positional'][1]
    if not isinstance(l2cmd, str) or l2cmd not in ('host', 'group', 'var'):
      if cmd == 'show' and l2cmd not in ('host', 'group', 'hosts', 'groups', 'tree'):
        return 'Wrong subcommand: %s' % l2cmd

    # Convert the 3rd positional argument in the "name" optional argument
    if args_dict['positional'].__len__() == 3 and cmd+l2cmd != 'addvar':
      args[2] = ('name', args_dict['positional'][2])
      return self.__validate_args( args )

    # Check the number of positional arguments
    if args_dict['positional'].__len__() > 3:
      return 'Too many positional arguments'

    # Initialize the posible options per command+subcommand and the posible option combinations
    l2cmd_opts = []
    l2cmd_combs = []
    if cmd == 'add':
      if l2cmd == 'host':
        l2cmd_opts = ['name', 'host', 'to_groups']
        l2cmd_combs = [
          ['name'],
          ['name', 'host'],
          ['name', 'to_groups'],
        ]
      elif l2cmd == 'group':
        l2cmd_opts = ['name', 'to_groups']
        l2cmd_combs = [
          ['name'],
          ['name', 'to_groups']
        ]
      elif l2cmd == 'var':
        l2cmd_opts = [ args[2][0], 'to_hosts', 'to_groups']
        l2cmd_combs = [
          [ args[2][0], 'to_hosts'],
          [ args[2][0], 'to_groups']
        ]

    elif cmd == 'edit':
      if l2cmd == 'host':
        l2cmd_opts = ['name', 'new_name', 'new_host']
        l2cmd_combs = [
          ['name', 'new_name'],
          ['name', 'new_host']
        ]
      elif l2cmd == 'group':
        l2cmd_opts = ['name', 'new_name']
        l2cmd_combs = [
          ['name', 'new_name']
        ]
      elif l2cmd == 'var':
        l2cmd_opts = ['name', 'new_name', 'new_value', 'in_hosts', 'in_groups']
        l2cmd_combs = [
          ['name', 'new_name', 'in_hosts'],
          ['name', 'new_name', 'in_groups'],
          ['name', 'new_value', 'in_hosts'],
          ['name', 'new_value', 'in_groups']
        ]

    elif cmd == 'del':
      if l2cmd == 'host':
        l2cmd_opts = ['name', 'from_groups']
        l2cmd_combs = [
          ['name'],
          ['name', 'from_groups'],
        ]
      elif l2cmd == 'group':
        l2cmd_opts = ['name', 'from_groups']
        l2cmd_combs = [
          ['name'],
          ['name', 'from_groups']
        ]
      elif l2cmd == 'var':
        l2cmd_opts = ['name', 'from_hosts', 'from_groups']
        l2cmd_combs = [
          ['name', 'from_hosts'],
          ['name', 'from_groups']
        ]

    elif cmd == 'show':
      if l2cmd in ['host', 'hosts']:
        l2cmd_opts = ['name']
        l2cmd_combs = [
          [],
          ['name'],
        ]
      elif l2cmd in ['group', 'groups']:
        l2cmd_opts = ['name']
        l2cmd_combs = [
          [],
          ['name']
        ]
      elif l2cmd in ['tree']:
        l2cmd_opts = ['name']
        l2cmd_combs = [
          ['name']
        ]

    else:
      return 'Wrong command: %s' % cmd

    if not l2cmd_opts and not l2cmd_combs:
      return 'Wrong subcommand %s' % l2cmd

    # Check optional arguments
    for a in args_dict['optional']:
      if a not in l2cmd_opts:
        return 'Invalid argument %s' % a
    valid=False
    pos_args = list(args_dict['optional'].keys())
    pos_args.sort()
    for c in l2cmd_combs:
      c.sort()
      if pos_args == c:
        valid=True
    if not valid:
      return 'Invalid arguments'

    # Return the args_dict with the optional and positional arguments
    return args_dict

  def precmd(self, line):
    if line == 'EOF':
      print('')
      self.do_exit(None)
      sys.exit(0)
    if line and line.split() and line.split()[0] not in self.allowed_commands:
      self.__error('Invalid command')
      return ''
    return line

  def __ok(self, msg):
    if USE_COLORS:
      print(C_BASE+"v   "+C_GOOD+'ok '+C_BASE+msg+C_RESET)
    else:
      print("v   ok "+msg)
    print('')

  def __info(self, msg):
    if USE_COLORS:
      print(C_BASE+"-   "+C_INFO+'info '+C_BASE+msg+C_RESET)
    else:
      print("-   info "+msg)
    print('')

  def __warn(self, msg):
    if USE_COLORS:
      print(C_BASE+"~   "+C_WARN+'warning '+C_BASE+msg+C_RESET)
    else:
      print("~   warning "+msg)
    print('')

  def __error(self, msg):
    if USE_COLORS:
      print(C_BASE+"^   "+C_FAIL+'error '+C_BASE+msg+C_RESET)
    else:
      print("^   error "+msg)
    print('')

  def __confirm(self, msg):
    if USE_COLORS:
      print(C_BASE+"·   "+C_INFO+'confirm '+C_BASE+msg+' [N/y]: '+C_RESET, end='')
    else:
      print("·   confirm "+msg+' [N/y]: ', end='')
    sys.stdout.flush()
    answer = sys.stdin.readline()
    if answer.lower().strip( '\n' ) in ('y', 'yes'):
      return True
    return False

  def do_exit(self, arg):
    'Exists Ansible Inventory console'
    readline.write_history_file( HISTORY_FILE )
    self.__close()
    self.__info('Deica logo!')
    return True

  def __print_group_tree(self, group, level=0, preline='', last_node=False):
      hosts = self.inventory.get_group_hosts( group )
      hosts.sort()
      if hosts:
        last_host = hosts[-1]
      child = self.inventory.get_group_children( group )
      child.sort()
      if child:
        last_child = child[-1]

      if hosts or child:
        g_fork = '┬'
      else:
        g_fork = '─'

      if last_node:
        g_link = '╰'
      else:
        g_link = '├'

      if level == 0:
        print('  ╭─'+'─'*group.__len__()+'─╮')
        print(' ╭┤ %s │' % C( group ))
        print(' │╰─'+'─'*group.__len__()+'─╯')
      else:
        if last_node:
          print( preline +'\b\b │' )
        else:
          print( preline )
        print( preline, end='' )
        print('\b\b %s' % g_link, end='')
        print('─%s─[%s]' % (g_fork, C( group )) )

      for h in hosts:
        print( preline, end='')
        if h == last_host and not child:
          print(' ╰', end='' )
        else:
          print(' ├', end='' )
        print('╴%s' % C( h ))

      for g in child:
        if last_child == g:
          prel = preline + '  '
        else:
          prel = preline + ' │'
        self.__print_group_tree( g, level+1, preline=prel, last_node=last_child==g)

  @console_handler
  def do_show(self, args):
    """
    show host(s) [[name=]HOST_REGEX]
    show group(s) [[name=]GROUPS_REGEX]
    show tree <[name=]GROUP>

    HOST: Domain name or IP of the host
    VAR_NAME: Variable name
    VAR_VALUE: Variable value
    {SOMETHING}_REGEX: Regular expression (i.e.: name=test_.* )
    {SOMETHING}_REGEX_LIST: Comma separated list of regular expressions (i.e.: in_groups=test[1-3],example.*)
    """
    args = self.__validate_args( args )
    if isinstance(args, str):
      self.__error( args )
      self.do_help('show')
      return False

    args_opt = args['optional']
    args_pos = args['positional']

    name = None
    if 'name' in args_opt:
      name = args_opt['name']

    # SHOW HOST
    if args_pos[1] in ['host', 'hosts']:
      if name:
        hosts = self.inventory.list_hosts( name )
      else:
        hosts = self.inventory.list_hosts()

      if not hosts:
        self.__warn('No host matched')
        return False

      max_n_len=0
      for n in hosts:
        cn = C(n)
        if cn.__len__() > max_n_len:
          max_n_len = cn.__len__()
      hosts.sort()

      host_line = ' ╭╴%%-%ds ' % max_n_len

      self.__info('Here is your hosts list')
      for host in hosts:
        print( host_line % C(host), end='' )

        print('[ ', end='')
        h_vars =  self.inventory.get_host_vars( host )
        h_vars_keys = []
        for v in h_vars:
          h_vars_keys.append( v )
        h_vars_keys.sort()
        for v in h_vars_keys:
          print( '%s=%s ' % (C(v), h_vars[v]), end='')
        print(']')

        print(' ╰──╴groups╶( ', end='')
        groups = self.inventory.get_host_groups( host )
        groups.sort()
        for g in groups:
          print( C(g)+' ', end='')
        print(')\n')

    # SHOW GROUP
    if args_pos[1] in ['group', 'groups']:
      if name:
        groups = self.inventory.list_groups(name)
      else:
        groups = self.inventory.list_groups()

      if not groups:
        self.__warn('No group matched')
        return False

      max_n_len=0
      for n in groups:
        cn = C(n)
        if cn.__len__() > max_n_len:
          max_n_len = cn.__len__()
      groups.sort()

      group_line = ' ╭╴%%-%ds [ ' % max_n_len

      self.__info('Here is your groups list')
      for n in groups:
        print( group_line % C(n), end='' )
        g_vars =  self.inventory.get_group_vars( n )
        for v in g_vars:
          print( '%s=%s ' % (C(v), g_vars[v]), end='')
        print(']')

        print(' ├──╴hosts╶( ', end='')
        for c in self.inventory.get_group_hosts( n ):
          print( C(c)+' ', end='')
        print(')')

        print(' ╰──╴child╶{ ', end='')
        for h in self.inventory.get_group_children( n ):
          print( C(h)+' ', end='')
        print('}\n')

    # SHOW TREE
    if args_pos[1] == 'tree':
      if name not in self.inventory.list_groups(name):
        self.__warn('No group matched')
        return False
      self.__info('Here is the group tree for %s' % C( name ))
      self.__print_group_tree( name )

  @console_handler
  def do_add(self, args):
    """
    add host  <[name=]HOST> [host=<host>[:<host_port>]]
    add host  <[name=]HOST_REGEX> <to_groups=GROUP_REGEX_LIST>
    add group <[name=]GROUP> [to_groups=GROUP_REGEX_LIST]
    add var   <VAR_NAME=VAR_VALUE> <to_hosts=HOST_REGEX_LIST>
    add var   <VAR_NAME=VAR_VALUE> <to_groups=GROUP_REGEX_LIST>

    HOST: Domain name or IP of the host
    VAR_NAME: Variable name
    VAR_VALUE: Variable value
    {SOMETHING}_REGEX: Regular expression (i.e.: name=test_.* )
    {SOMETHING}_REGEX_LIST: Comma separated list of regular expressions (i.e.: in_groups=test[1-3],example.*)
    """
    args = self.__validate_args( args )
    if isinstance(args, str):
      self.__error( args )
      self.do_help('add')
      return False

    args_opt = args['optional']
    args_pos = args['positional']

    if 'to_groups' in args_opt:
      to_groups = args_opt['to_groups'].split(',')
    else:
      to_groups = []

    if 'to_hosts' in args_opt:
      to_hosts = args_opt['to_hosts'].split(',')
    else:
      to_hosts = []

    # ADD HOST
    if args_pos[1] == 'host':
      name = args_opt['name']
      host = None
      port = None

      if 'host' in args_opt:
        if ':' in args_opt['host']:
          host, port = args_opt['host'].split(':')
        else:
          host = args_opt['host']

      if not to_groups:
        self.inventory.add_host( name, host, port )
        self.__ok('Host %s added' % C(name))

      # ADD HOST: TO_GROUPS
      else:
        for g_regex in to_groups:
          self.inventory.add_hosts_to_groups( name, g_regex )
        self.__ok('Host %s added to groups' % C(name))

    # ADD GROUP
    elif args_pos[1] == 'group':
      group = args_opt['name']

      if to_groups:
        for g_regex in to_groups:
          self.inventory.add_group_to_groups( group, g_regex )
        self.__ok('Group %s added to groups' % C(group))
      else:
        self.inventory.add_group( group )
        self.__ok('Group %s added' % C(group))

    # ADD VAR
    elif args_pos[1] == 'var':
      for v in args_opt:
        if v not in ['to_hosts', 'to_groups']:
          v_name = v
          v_value = args_opt[v]

      if to_groups:
        for g_regex in to_groups:
          self.inventory.add_var_to_groups( v_name, v_value, g_regex )
        self.__ok('Var %s added to groups with value %s' % ( C(v_name), C(v_value) ) )

      if to_hosts:
        for h_regex in to_hosts:
          self.inventory.add_var_to_hosts( v_name, v_value, h_regex )
        self.__ok('Var %s added to hosts with value %s' % ( C(v_name), C(v_value) ) )

  @console_handler
  def do_edit(self, args):
    """
    edit host  <[name=]HOST> <new_name=NEW_NAME>
    edit host  <[name=]HOST> <new_host=NEW_HOST>
    edit group <[name=]GROUP> <new_name=NEW_NAME>
    edit var   <[name=]VAR_NAME> <new_name=NEW_NAME> <[in_hosts=HOST_REGEX_LIST | in_groups=GROUP_REGEX_LIST]>
    edit var   <[name=]VAR_NAME> <new_value=NEW_VALUE> <[in_hosts=HOST_REGEX_LIST | in_groups=GROUP_REGEX_LIST]>

    HOST: Domain name or IP of the host
    VAR_NAME: Variable name
    VAR_VALUE: Variable value
    {SOMETHING}_REGEX: Regular expression (i.e.: name=test_.* )
    {SOMETHING}_REGEX_LIST: Comma separated list of regular expressions (i.e.: in_groups=test[1-3],example.*)
    """
    args = self.__validate_args( args )
    if isinstance(args, str):
      self.__error( args )
      self.do_help('edit')
      return False

    args_opt = args['optional']
    args_pos = args['positional']

    if 'in_groups' in args_opt:
      in_groups = args_opt['in_groups'].split(',')
    else:
      in_groups = []

    if 'in_hosts' in args_opt:
      in_hosts = args_opt['in_hosts'].split(',')
    else:
      in_hosts = []

    # EDIT HOST
    if args_pos[1] == 'host':
      name = args_opt['name']

      if 'new_name' in args_opt:
        msg = self.inventory.rename_host( name, args_opt['new_name'] )
        if msg:
          self.__warn(msg)
        else:
          self.__ok('Host %s renamed to %s' % ( C(name), C(args_opt['new_name'])))

      if 'new_host' in args_opt:
        if ':' in args_opt['new_host']:
          host, port = args_opt['new_host'].split(':')
        else:
          host = args_opt['new_host']
          port = None

        self.inventory.change_host( name, host, port )
        self.__ok('Host address for %s changed to %s' % ( C(name), C(args_opt['new_host'])))

    # EDIT GROUP
    if args_pos[1] == 'group':
      g = args_opt['name']
      msg = self.inventory.rename_group( g, args_opt['new_name'] )
      if msg:
        self.__info(msg)
      else:
        self.__ok('Group %s renamed to %s' % (g, args_opt['new_name']))

    # EDIT VAR
    if args_pos[1] == 'var':
      v_name = args_opt['name']
      new_name = None
      new_value = None
      if 'new_name' in args_opt:
        new_name = args_opt['new_name']
      if 'new_value' in args_opt:
        new_value = args_opt['new_value']

      if in_groups:
        if new_name:
          for g_regex in in_groups:
            self.inventory.rename_group_var(v_name, new_name, g_regex)
          self.__ok('Variable %s renamed to %s in selected groups' % (C(v_name), C(new_name)))
        if new_value:
          for g_regex in in_groups:
            self.inventory.change_group_var(v_name, new_value, g_regex)
          self.__ok('Variable %s changed to %s in selected groups' % (C(v_name), C(new_value)))

      if in_hosts:
        if new_name:
          for h_regex in in_hosts:
            self.inventory.rename_host_var(v_name, new_name, h_regex)
          self.__ok('Variable %s renamed to %s in selected hosts' % (C(v_name), C(new_name)))
        if new_value:
          for h_regex in in_hosts:
            self.inventory.change_host_var(v_name, new_value, h_regex)
          self.__ok('Variable %s changed to %s in selected hosts' % (C(v_name), C(new_value)))

  @console_handler
  def do_del(self, args):
    """
    del host  <[name=]HOST_REGEX> [from_groups=GROUP_REGEX_LIST]
    del group <[name=]GROUP_REGEX> [from_groups=GROUP_REGEX_LIST]
    del var   <[name=]VAR_NAME> <[from_hosts=HOST_REGEX_LIST | from_groups=GROUP_REGEX_LIST]>

    HOST: Domain name or IP of the host
    VAR_NAME: Variable name
    VAR_VALUE: Variable value
    {SOMETHING}_REGEX: Regular expression (i.e.: name=test_.* )
    {SOMETHING}_REGEX_LIST: Comma separated list of regular expressions (i.e.: in_groups=test[1-3],example.*)
    """
    args = self.__validate_args( args )
    if isinstance(args, str):
      self.__error( args )
      self.do_help('del')
      return False

    args_opt = args['optional']
    args_pos = args['positional']

    if 'from_groups' in args_opt:
      from_groups = args_opt['from_groups'].split(',')
    else:
      from_groups = []

    if 'from_hosts' in args_opt:
      from_hosts = args_opt['from_hosts'].split(',')
    else:
      from_hosts = []

    name = args_opt['name']

    # DEL HOST
    if args_pos[1] == 'host':
      hosts = self.inventory.list_hosts( name )
      hosts.sort()
      c_hosts = []
      for h in hosts:
        c_hosts.append( C( h ) )
      if not from_groups:
        if not hosts:
          self.__warn('Host pattern %s does not match any host' % C(name))
        elif self.__confirm('The following hosts will be permanently removed: %s.\n            Do you want to proceed?' % ', '.join( c_hosts ) ):
          for h in hosts:
            self.inventory.remove_host( h )
          self.__ok('The hosts have been removed')
        else:
          return False
      else:
        del_groups = []
        for g_regex in from_groups:
          gs = self.inventory.list_groups( g_regex )
          del_groups += gs

        self.__info('The host %s would be removed from the following groups:' % C(name))
        for g in del_groups:
          print( ' '+C(g), end='')
        print('\n')
        if self.__confirm('Do you want to proceed?'):
          self.inventory.remove_host( name, from_groups=del_groups )
          self.__ok('Host %s removed from groups' % C(name))

    # DEL GROUP
    if args_pos[1] == 'group':
      if not from_groups:
        groups = self.inventory.list_groups( name )
        groups.sort()
        c_groups = []
        for g in groups:
          c_groups.append( C( g ) )
        if not groups:
          self.__warn('Group pattern %s does not match any group' % C(name))
        elif self.__confirm('The following groups will be permanently removed: %s.\n            Do you want to proceed?' % ', '.join( c_groups ) ):
          some_error = False
          for g in groups:
            msg = self.inventory.remove_group( g )
            if msg:
              some_error = True
              self.__warn( msg )
          if not some_error:
            self.__ok('The groups have been removed')
        else:
          return False
      else:
        del_groups = []
        for g_regex in from_groups:
          gs = self.inventory.list_groups( g_regex )
          del_groups += gs

        self.__info('The group %s would be removed from the following groups:' % C(name))
        for g in del_groups:
          print( ' '+C(g), end='')
        print('\n')
        if self.__confirm('Do you want to proceed?'):
          self.inventory.remove_group( name, from_groups=del_groups )
          self.__ok('Group %s removed from groups' % C(name))

    # DEL VAR
    if args_pos[1] == 'var':
      if from_groups:
        del_groups = []
        for g_regex in from_groups:
          gs = self.inventory.list_groups( g_regex )
          del_groups += gs
        self.__info('The variable %s would be removed from the following groups:' % C(name))
        for g in del_groups:
          print( ' '+C(g), end='')
        print('\n')
        if self.__confirm('Do you want to proceed?'):
          for g in del_groups:
            self.inventory.remove_group_var( name, g )
          self.__ok('Variable %s removed from groups' % C(name))
      elif from_hosts:
        del_hosts = []
        for h_regex in from_hosts:
          hs = self.inventory.list_hosts( h_regex )
          del_hosts += hs
        self.__info('The variable %s would be removed from the following hosts:' % C(name))
        for h in del_hosts:
          print( ' '+C(h), end='')
        print('\n')
        if self.__confirm('Do you want to proceed?'):
          for h in del_hosts:
            self.inventory.remove_host_var( name, h )
          self.__ok('Variable %s removed from hosts' % C(name))

  def completedefault(self, text, line, begidx, endidx):
    current_line=line.split()
    cmd = current_line[ 0 ]
    cmd_map = {
      'add' : {
        'host' : { 'host': None, 'to_groups': None },
        'group': { 'to_groups': None },
        'var'  : { 'to_hosts': None, 'to_groups': None }
      },
      'del' : {
        'host' : { 'from_groups': None },
        'group': { 'from_groups': None },
        'var'  : { 'from_hosts': None, 'from_groups': None }
      },
      'edit': {
        'host' : { 'new_name': None, 'new_host': None },
        'group': { 'new_name': None },
        'var'  : {
          'new_name' : { 'in_hosts': None, 'in_groups': None },
          'new_value': { 'in_hosts': None, 'in_groups': None },
        }
      },
      'show': { 'host': None, 'group': None, 'tree': None },
    }

    def __comp( _kind, _text ):
      if _kind == 'host':
        return self.inventory.local_list_hosts( _text+'.*' )
      elif _kind == 'var':
        return self.inventory.local_list_vars( _text+'.*' )
      else:
        return self.inventory.local_list_groups( _text+'.*' )

    if cmd in cmd_map:
      if current_line.__len__() > 1:
        kind = current_line[ 1 ]
        if kind not in cmd_map[ cmd ].keys():
          for sc in cmd_map[ cmd ]:
            if sc.startswith( kind ):
              return [ sc+' ' ]
        else:
          if cmd != 'show':
            options = list(cmd_map[ cmd ][ kind ].keys())

          if current_line.__len__() > 2:
            target = current_line[ 2 ]
          else:
            target = None

          if current_line.__len__() > 3:
            subtarget = current_line[ 3 ]
            options = list(cmd_map[ cmd ][ kind ].keys())
          else:
            subtarget = None

          if current_line.__len__() > 4 or (current_line.__len__() == 4 and not text):
            if cmd == 'edit' and kind == 'var' and subtarget.startswith('new_'):
              options = list(cmd_map[ cmd ][ kind ][ subtarget.split('=')[0] ].keys())
              if current_line.__len__() > 4:
                subtarget = current_line[ 4 ]
              else:
                subtarget = None

          if subtarget is not None:
            if 'groups=' in text or 'hosts=' in text:
              part = text.split('=')[1].split(',')[ -1 ]
              prepart = text.rpartition(',')[0]
              if prepart:
                prepart+=','
              else:
                prepart = text.split('=')[0]+'='
              if 'hosts=' in text:
                return [ prepart+x for x in __comp('host', part) ]
              return [ prepart+x for x in __comp('group', part) ]
            else:
              opt = []
              for o in [ op+'=' for op in options ]:
                if o.startswith( text ):
                  opt.append( o )
              return opt or options

          if target is not None:
            if text:
              return __comp( kind, text )
            return options
          return __comp( kind, text )
      else:
        return list(cmd_map[ cmd ].keys())
    return []
# )


if __name__ == '__main__':
  # Create needed directories and files
  check_requirements()

  # Read configuration
  config = configparser.ConfigParser()
  config.read( CONFIG_PATH )

  print( list( config.keys() ) )
  sys.exit( 1 )

  # Parse configuration
  USE_COLORS = config['global'].getboolean('use_colors')
  if config['global']['backend'].lower() == 'file':
    BACKEND = file_backend
    BACKEND_PARAMS = os.path.expanduser( config['file_backend']['path'].strip('"\'') )
  elif config['global']['backend'].lower() == 'redis':
    import redis
    BACKEND = redis_backend
    BACKEND_PARAMS = { 'host': config['redis_backend']['host'] }
    port = config['redis_backend']['port']
    password = config['redis_backend']['password']
    if port:
      BACKEND_PARAMS['port'] = port
    if password:
      BACKEND_PARAMS['password'] = password

  parser = argparse.ArgumentParser( description='Ansible Inventory manager' )
  group = parser.add_mutually_exclusive_group()
  group.add_argument('--import', dest='inventory', action='store',
                      help='Import an existing ansible inventory. It must be in the ansible dynamic inventory JSON format.')
  group.add_argument('--batch', dest='command', action='store',
                     help='Execute an Ansible Inventory command in batch mode. (i.e. "add host test host=1.2.3.4" ).')
  group.add_argument('--list', dest='list', action='store_true',
                     help='Used by Ansible. Dumps the current inventory.')
  group.add_argument('--host', dest='host', action='store',
                     help='Used by Ansible. Dumps the inventory information of a known host.')
  args = parser.parse_args()

  # Import
  if args.inventory:
    print(" This will overwrite the current inventory. Are you sure you want to continue? [N/y]: ", end="")
    sys.stdout.flush()
    ans = sys.stdin.readline()
    if ans.replace('\n', '').lower() in ('y', 'yes'):
      json_file_import = args.inventory
      if not os.path.exists( json_file_import ):
        print("File %s does not exist." % json_file_import)
        sys.exit( 1 )
    else:
      sys.exit( 1 )

    inventory = Inventory( file_backend( json_file_import ) )
    inventory.backend = BACKEND( BACKEND_PARAMS )
    inventory.save()
    sys.exit( 0 )

  # Load inventory and create console
  inventory = Inventory( BACKEND( BACKEND_PARAMS ) )
  console = Console( inventory )

  # Import
  if args.command:
    USE_COLORS = False
    cmd = args.command
    console.onecmd( cmd )
    sys.exit( 0 )

  # Ansible requests
  if args.list:
    print( inventory.get_ansible_json() )
    sys.exit( 0 )

  if args.host:
    host = args.host
    print( inventory.get_ansible_host_json(host) )
    sys.exit( 0 )

  # Main console loop
  console.cmdloop()
