2012-04-13 14:39:54 +02:00
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
#
# This file is part of Ansible
#
# Ansible 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, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible 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 Ansible. If not, see <http://www.gnu.org/licenses/>.
#############################################
import fnmatch
2012-05-06 20:47:05 +02:00
import os
2012-04-13 14:39:54 +02:00
2012-05-06 20:47:05 +02:00
import subprocess
2012-05-26 01:34:13 +02:00
import ansible . constants as C
from ansible . inventory . ini import InventoryParser
from ansible . inventory . script import InventoryScript
from ansible . inventory . group import Group
from ansible . inventory . host import Host
2012-04-13 14:39:54 +02:00
from ansible import errors
from ansible import utils
2012-10-27 01:55:59 +02:00
# FIXME, adapt.
dirname = os . path . dirname ( __file__ )
vars_plugin_list = utils . import_plugins ( os . path . join ( dirname , ' vars_plugins ' ) )
for i in reversed ( C . DEFAULT_VARS_PLUGIN_PATH . split ( os . pathsep ) ) :
vars_plugin_list . update ( utils . import_plugins ( i ) )
2012-04-13 14:39:54 +02:00
class Inventory ( object ) :
2012-08-07 02:07:02 +02:00
"""
2012-05-05 22:31:03 +02:00
Host inventory for ansible .
2012-04-13 14:39:54 +02:00
"""
2012-08-18 15:52:13 +02:00
__slots__ = [ ' host_list ' , ' groups ' , ' _restriction ' , ' _also_restriction ' , ' _subset ' , ' _is_script ' ,
2012-10-27 01:55:59 +02:00
' parser ' , ' _vars_per_host ' , ' _vars_per_group ' , ' _hosts_cache ' , ' _groups_list ' ,
' _vars_plugins ' ]
2012-07-22 17:40:02 +02:00
2012-05-05 22:31:03 +02:00
def __init__ ( self , host_list = C . DEFAULT_HOST_LIST ) :
2012-04-13 14:39:54 +02:00
2012-05-08 06:27:37 +02:00
# the host file file, or script path, or list of hosts
# if a list, inventory data will NOT be loaded
2012-05-06 20:47:05 +02:00
self . host_list = host_list
2012-05-08 05:16:20 +02:00
2012-07-22 17:53:19 +02:00
# caching to avoid repeated calculations, particularly with
# external inventory scripts.
2012-08-07 02:07:02 +02:00
2012-07-22 17:53:19 +02:00
self . _vars_per_host = { }
self . _vars_per_group = { }
self . _hosts_cache = { }
2012-09-08 00:07:52 +02:00
self . _groups_list = { }
2012-07-22 17:53:19 +02:00
2012-05-08 05:16:20 +02:00
# the inventory object holds a list of groups
2012-05-05 22:31:03 +02:00
self . groups = [ ]
2012-08-07 02:07:02 +02:00
2012-05-08 05:16:20 +02:00
# a list of host(names) to contain current inquiries to
2012-05-05 22:31:03 +02:00
self . _restriction = None
2012-08-18 15:52:13 +02:00
self . _also_restriction = None
2012-08-10 08:45:29 +02:00
self . _subset = None
2012-05-08 05:16:20 +02:00
# whether the inventory file is a script
2012-05-06 20:47:05 +02:00
self . _is_script = False
2012-05-10 05:26:45 +02:00
if type ( host_list ) in [ str , unicode ] :
if host_list . find ( " , " ) != - 1 :
2012-07-15 15:32:47 +02:00
host_list = host_list . split ( " , " )
2012-07-18 22:56:41 +02:00
host_list = [ h for h in host_list if h and h . strip ( ) ]
2012-05-10 05:26:45 +02:00
2012-05-08 06:27:37 +02:00
if type ( host_list ) == list :
all = Group ( ' all ' )
self . groups = [ all ]
for x in host_list :
if x . find ( " : " ) != - 1 :
tokens = x . split ( " : " , 1 )
all . add_host ( Host ( tokens [ 0 ] , tokens [ 1 ] ) )
else :
all . add_host ( Host ( x ) )
2012-09-24 20:47:59 +02:00
elif utils . is_executable ( host_list ) :
2012-05-08 05:16:20 +02:00
self . _is_script = True
self . parser = InventoryScript ( filename = host_list )
self . groups = self . parser . groups . values ( )
else :
data = file ( host_list ) . read ( )
if not data . startswith ( " --- " ) :
self . parser = InventoryParser ( filename = host_list )
2012-05-06 20:47:05 +02:00
self . groups = self . parser . groups . values ( )
2012-08-07 02:07:02 +02:00
else :
raise errors . AnsibleError ( " YAML inventory support is deprecated in 0.6 and removed in 0.7, see the migration script in examples/scripts in the git checkout " )
2012-10-27 01:55:59 +02:00
self . _vars_plugins = [ i . VarsModule ( self ) for i in vars_plugin_list . values ( ) ]
2012-05-05 22:31:03 +02:00
def _match ( self , str , pattern_str ) :
return fnmatch . fnmatch ( str , pattern_str )
def get_hosts ( self , pattern = " all " ) :
2012-08-11 17:36:59 +02:00
"""
find all host names matching a pattern string , taking into account any inventory restrictions or
applied subsets .
"""
2012-09-11 19:00:40 +02:00
# process patterns
if isinstance ( pattern , list ) :
pattern = ' ; ' . join ( pattern )
2012-05-05 22:31:03 +02:00
patterns = pattern . replace ( " ; " , " : " ) . split ( " : " )
2012-08-11 17:36:59 +02:00
positive_patterns = [ p for p in patterns if not p . startswith ( " ! " ) ]
negative_patterns = [ p for p in patterns if p . startswith ( " ! " ) ]
# find hosts matching positive patterns
hosts = self . _get_hosts ( positive_patterns )
# exclude hosts mentioned in a negative pattern
if len ( negative_patterns ) :
2012-09-26 01:16:25 +02:00
exclude_hosts = [ h . name for h in self . _get_hosts ( negative_patterns ) ]
hosts = [ h for h in hosts if h . name not in exclude_hosts ]
2012-08-11 17:36:59 +02:00
# exclude hosts not in a subset, if defined
if self . _subset :
positive_subsetp = [ p for p in self . _subset if not p . startswith ( " ! " ) ]
negative_subsetp = [ p for p in self . _subset if p . startswith ( " ! " ) ]
if len ( positive_subsetp ) :
2012-08-28 03:49:12 +02:00
positive_subset = [ h . name for h in self . _get_hosts ( positive_subsetp ) ]
hosts = [ h for h in hosts if ( h . name in positive_subset ) ]
2012-08-11 17:36:59 +02:00
if len ( negative_subsetp ) :
2012-08-28 03:49:12 +02:00
negative_subset = [ h . name for h in self . _get_hosts ( negative_subsetp ) ]
hosts = [ h for h in hosts if ( h . name not in negative_subset ) ]
2012-08-11 17:36:59 +02:00
# exclude hosts mentioned in any restriction (ex: failed hosts)
if self . _restriction is not None :
2012-08-11 18:20:16 +02:00
hosts = [ h for h in hosts if h . name in self . _restriction ]
2012-08-18 15:52:13 +02:00
if self . _also_restriction is not None :
hosts = [ h for h in hosts if h . name in self . _also_restriction ]
2012-08-11 17:36:59 +02:00
2012-08-11 19:49:18 +02:00
return sorted ( hosts , key = lambda x : x . name )
2012-08-11 17:36:59 +02:00
def _get_hosts ( self , patterns ) :
"""
finds hosts that postively match a particular list of patterns . Does not
take into account negative matches .
"""
by_pattern = { }
for p in patterns :
( name , enumeration_details ) = self . _enumeration_info ( p )
2012-08-11 19:49:18 +02:00
hpat = self . _hosts_in_unenumerated_pattern ( name )
hpat = sorted ( hpat , key = lambda x : x . name )
by_pattern [ p ] = hpat
2012-08-11 17:36:59 +02:00
ranged = { }
for ( pat , hosts ) in by_pattern . iteritems ( ) :
ranged [ pat ] = self . _apply_ranges ( pat , hosts )
results = [ ]
for ( pat , hosts ) in ranged . iteritems ( ) :
results . extend ( hosts )
return list ( set ( results ) )
def _enumeration_info ( self , pattern ) :
"""
returns ( pattern , limits ) taking a regular pattern and finding out
which parts of it correspond to start / stop offsets . limits is
a tuple of ( start , stop ) or None
"""
if not " [ " in pattern :
return ( pattern , None )
( first , rest ) = pattern . split ( " [ " )
2012-08-11 19:49:18 +02:00
rest = rest . replace ( " ] " , " " )
2012-08-11 17:36:59 +02:00
if not " - " in rest :
raise errors . AnsibleError ( " invalid pattern: %s " % pattern )
( left , right ) = rest . split ( " - " , 1 )
return ( first , ( left , right ) )
def _apply_ranges ( self , pat , hosts ) :
2012-08-11 19:49:18 +02:00
"""
given a pattern like foo , that matches hosts , return all of hosts
given a pattern like foo [ 0 : 5 ] , where foo matches hosts , return the first 6 hosts
"""
2012-08-11 17:36:59 +02:00
( loose_pattern , limits ) = self . _enumeration_info ( pat )
if not limits :
return hosts
2012-08-11 19:49:18 +02:00
( left , right ) = limits
enumerated = enumerate ( hosts )
if left == ' ' :
left = 0
if right == ' ' :
right = 0
left = int ( left )
right = int ( right )
enumerated = [ h for ( i , h ) in enumerated if i > = left and i < = right ]
return enumerated
2012-08-11 17:36:59 +02:00
# TODO: cache this logic so if called a second time the result is not recalculated
def _hosts_in_unenumerated_pattern ( self , pattern ) :
""" Get all host names matching the pattern """
hosts = { }
# ignore any negative checks here, this is handled elsewhere
pattern = pattern . replace ( " ! " , " " )
2012-05-05 22:31:03 +02:00
2012-05-08 05:16:20 +02:00
groups = self . get_groups ( )
2012-08-11 17:36:59 +02:00
for group in groups :
for host in group . get_hosts ( ) :
if pattern == ' all ' or self . _match ( group . name , pattern ) or self . _match ( host . name , pattern ) :
hosts [ host . name ] = host
2012-05-05 22:31:03 +02:00
return sorted ( hosts . values ( ) , key = lambda x : x . name )
2012-08-15 01:48:33 +02:00
def groups_for_host ( self , host ) :
results = [ ]
groups = self . get_groups ( )
for group in groups :
for hostn in group . get_hosts ( ) :
if host == hostn . name :
results . append ( group )
continue
return results
2012-09-08 00:07:52 +02:00
def groups_list ( self ) :
if not self . _groups_list :
groups = { }
for g in self . groups :
groups [ g . name ] = [ h . name for h in g . get_hosts ( ) ]
ancestors = g . get_ancestors ( )
for a in ancestors :
groups [ a . name ] = [ h . name for h in a . get_hosts ( ) ]
self . _groups_list = groups
return self . _groups_list
2012-05-05 22:31:03 +02:00
def get_groups ( self ) :
return self . groups
def get_host ( self , hostname ) :
2012-07-22 17:53:19 +02:00
if hostname not in self . _hosts_cache :
self . _hosts_cache [ hostname ] = self . _get_host ( hostname )
return self . _hosts_cache [ hostname ]
def _get_host ( self , hostname ) :
2012-05-05 22:31:03 +02:00
for group in self . groups :
2012-07-15 15:32:47 +02:00
for host in group . get_hosts ( ) :
if hostname == host . name :
2012-05-05 22:31:03 +02:00
return host
return None
def get_group ( self , groupname ) :
for group in self . groups :
if group . name == groupname :
return group
return None
2012-08-07 02:07:02 +02:00
2012-05-05 22:31:03 +02:00
def get_group_variables ( self , groupname ) :
2012-07-22 17:53:19 +02:00
if groupname not in self . _vars_per_group :
self . _vars_per_group [ groupname ] = self . _get_group_variables ( groupname )
return self . _vars_per_group [ groupname ]
def _get_group_variables ( self , groupname ) :
2012-05-05 22:31:03 +02:00
group = self . get_group ( groupname )
if group is None :
raise Exception ( " group not found: %s " % groupname )
return group . get_variables ( )
def get_variables ( self , hostname ) :
2012-07-22 17:53:19 +02:00
if hostname not in self . _vars_per_host :
self . _vars_per_host [ hostname ] = self . _get_variables ( hostname )
return self . _vars_per_host [ hostname ]
def _get_variables ( self , hostname ) :
2012-05-06 20:47:05 +02:00
2012-10-19 16:26:12 +02:00
host = self . get_host ( hostname )
if host is None :
raise errors . AnsibleError ( " host not found: %s " % hostname )
2012-10-27 01:55:59 +02:00
vars = { }
for ip in self . _vars_plugins :
updated = ip . run ( host )
vars . update ( updated )
2012-05-06 20:47:05 +02:00
if self . _is_script :
cmd = subprocess . Popen (
2012-08-07 02:07:02 +02:00
[ self . host_list , " --host " , hostname ] ,
stdout = subprocess . PIPE ,
2012-05-06 20:47:05 +02:00
stderr = subprocess . PIPE
)
( out , err ) = cmd . communicate ( )
results = utils . parse_json ( out )
2012-07-31 01:26:46 +02:00
# FIXME: this is a bit redundant with host.py and should share code
results [ ' inventory_hostname ' ] = hostname
results [ ' inventory_hostname_short ' ] = hostname . split ( ' . ' ) [ 0 ]
groups = [ g . name for g in host . get_groups ( ) if g . name != ' all ' ]
results [ ' group_names ' ] = sorted ( groups )
2012-10-27 01:55:59 +02:00
vars . update ( results )
2012-10-19 16:26:12 +02:00
else :
2012-10-27 01:55:59 +02:00
vars . update ( host . get_variables ( ) )
return vars
2012-05-05 22:31:03 +02:00
def add_group ( self , group ) :
self . groups . append ( group )
2012-04-13 14:39:54 +02:00
def list_hosts ( self , pattern = " all " ) :
2012-05-05 22:31:03 +02:00
return [ h . name for h in self . get_hosts ( pattern ) ]
def list_groups ( self ) :
2012-08-11 19:49:18 +02:00
return sorted ( [ g . name for g in self . groups ] , key = lambda x : x . name )
2012-04-13 14:39:54 +02:00
2012-08-18 15:52:13 +02:00
# TODO: remove this function
2012-05-08 05:16:20 +02:00
def get_restriction ( self ) :
return self . _restriction
2012-08-10 08:45:29 +02:00
def restrict_to ( self , restriction ) :
"""
Restrict list operations to the hosts given in restriction . This is used
to exclude failed hosts in main playbook code , don ' t use this for other
reasons .
"""
2012-07-15 15:32:47 +02:00
if type ( restriction ) != list :
restriction = [ restriction ]
self . _restriction = restriction
2012-04-13 14:39:54 +02:00
2012-08-18 15:52:13 +02:00
def also_restrict_to ( self , restriction ) :
"""
Works like restict_to but offers an additional restriction . Playbooks use this
to implement serial behavior .
"""
if type ( restriction ) != list :
restriction = [ restriction ]
self . _also_restriction = restriction
2012-08-10 08:45:29 +02:00
def subset ( self , subset_pattern ) :
"""
Limits inventory results to a subset of inventory that matches a given
pattern , such as to select a given geographic of numeric slice amongst
a previous ' hosts ' selection that only select roles , or vice versa .
Corresponds to - - limit parameter to ansible - playbook
"""
if subset_pattern is None :
self . _subset = None
else :
2012-10-23 03:24:25 +02:00
subset_pattern = subset_pattern . replace ( ' , ' , ' : ' )
2012-08-11 17:36:59 +02:00
self . _subset = subset_pattern . replace ( " ; " , " : " ) . split ( " : " )
2012-08-10 08:45:29 +02:00
2012-04-13 14:39:54 +02:00
def lift_restriction ( self ) :
""" Do not restrict list operations """
2012-05-08 05:16:20 +02:00
self . _restriction = None
2012-08-18 15:52:13 +02:00
def lift_also_restriction ( self ) :
""" Clears the also restriction """
self . _also_restriction = None
2012-07-20 15:43:45 +02:00
def is_file ( self ) :
""" did inventory come from a file? """
if not isinstance ( self . host_list , basestring ) :
return False
return os . path . exists ( self . host_list )
def basedir ( self ) :
""" if inventory came from a file, what ' s the directory? """
if not self . is_file ( ) :
return None
return os . path . dirname ( self . host_list )