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-11-27 16:36:58 +01:00
import re
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
2013-02-27 21:24:45 +01:00
from ansible . inventory . dir import InventoryDirectory
2012-05-26 01:34:13 +02:00
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
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
"""
2013-02-27 20:35:08 +01:00
__slots__ = [ ' host_list ' , ' groups ' , ' _restriction ' , ' _also_restriction ' , ' _subset ' ,
2013-04-20 15:59:40 +02:00
' parser ' , ' _vars_per_host ' , ' _vars_per_group ' , ' _hosts_cache ' , ' _groups_list ' ,
2013-06-01 16:38:16 +02:00
' _vars_plugins ' , ' _playbook_basedir ' ]
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
2013-06-01 16:38:16 +02:00
# to be set by calling set_playbook_basedir by ansible-playbook
self . _playbook_basedir = None
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
2013-05-23 18:33:29 +02:00
if isinstance ( host_list , basestring ) :
2013-05-23 18:37:30 +02:00
if " , " in host_list :
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
2013-05-23 18:33:29 +02:00
if isinstance ( host_list , list ) :
2013-03-04 12:37:15 +01:00
self . parser = None
2012-05-08 06:27:37 +02:00
all = Group ( ' all ' )
self . groups = [ all ]
for x in host_list :
2013-05-23 18:37:30 +02:00
if " : " in x :
tokens = x . split ( " : " , 1 )
2012-05-08 06:27:37 +02:00
all . add_host ( Host ( tokens [ 0 ] , tokens [ 1 ] ) )
else :
all . add_host ( Host ( x ) )
2013-02-04 05:19:37 +01:00
elif os . path . exists ( host_list ) :
2013-02-27 21:24:45 +01:00
if os . path . isdir ( host_list ) :
2013-02-28 16:58:09 +01:00
# Ensure basedir is inside the directory
self . host_list = os . path . join ( self . host_list , " " )
2013-02-27 21:24:45 +01:00
self . parser = InventoryDirectory ( filename = host_list )
self . groups = self . parser . groups . values ( )
elif utils . is_executable ( host_list ) :
2013-02-04 05:19:37 +01:00
self . parser = InventoryScript ( 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 :
2013-02-04 05:19:37 +01:00
data = file ( host_list ) . read ( )
if not data . startswith ( " --- " ) :
self . parser = InventoryParser ( filename = host_list )
self . groups = self . parser . groups . values ( )
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 " )
2013-02-28 16:58:09 +01:00
2013-03-01 23:22:07 +01:00
utils . plugins . vars_loader . add_directory ( self . basedir ( ) , with_subdir = True )
2013-02-04 05:19:37 +01:00
else :
2013-02-09 17:37:55 +01:00
raise errors . AnsibleError ( " Unable to find an inventory file, specify one with -i ? " )
2012-08-07 02:07:02 +02:00
2013-04-20 15:59:40 +02:00
self . _vars_plugins = [ x for x in utils . plugins . vars_loader . all ( self ) ]
2012-05-05 22:31:03 +02:00
def _match ( self , str , pattern_str ) :
2012-11-27 16:36:58 +01:00
if pattern_str . startswith ( ' ~ ' ) :
return re . search ( pattern_str [ 1 : ] , str )
else :
return fnmatch . fnmatch ( str , pattern_str )
2012-05-05 22:31:03 +02:00
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-12-10 15:54:07 +01:00
hosts = self . _get_hosts ( patterns )
2012-08-11 17:36:59 +02:00
# exclude hosts not in a subset, if defined
if self . _subset :
2012-12-10 15:54:07 +01:00
subset = self . _get_hosts ( self . _subset )
hosts . intersection_update ( 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 ) :
2012-12-10 15:54:07 +01:00
"""
finds hosts that match a list of patterns . Handles negative
matches as well as intersection matches .
2012-08-11 17:36:59 +02:00
"""
2012-12-10 15:54:07 +01:00
hosts = set ( )
2012-08-11 17:36:59 +02:00
for p in patterns :
2012-12-10 15:54:07 +01:00
if p . startswith ( " ! " ) :
# Discard excluded hosts
hosts . difference_update ( self . __get_hosts ( p ) )
elif p . startswith ( " & " ) :
# Only leave the intersected hosts
hosts . intersection_update ( self . __get_hosts ( p ) )
else :
# Get all hosts from both patterns
hosts . update ( self . __get_hosts ( p ) )
return hosts
2012-08-11 17:36:59 +02:00
2012-12-10 15:54:07 +01:00
def __get_hosts ( self , pattern ) :
"""
finds hosts that postively match a particular pattern . Does not
take into account negative matches .
"""
2012-08-11 17:36:59 +02:00
2012-12-10 15:54:07 +01:00
( name , enumeration_details ) = self . _enumeration_info ( pattern )
hpat = self . _hosts_in_unenumerated_pattern ( name )
hpat = sorted ( hpat , key = lambda x : x . name )
2012-08-11 17:36:59 +02:00
2012-12-10 15:54:07 +01:00
return set ( self . _apply_ranges ( pattern , hpat ) )
2012-08-11 17:36:59 +02:00
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
"""
2012-11-27 16:36:58 +01:00
if not " [ " in pattern or pattern . startswith ( ' ~ ' ) :
2012-08-11 17:36:59 +02:00
return ( pattern , None )
( first , rest ) = pattern . split ( " [ " )
2012-08-11 19:49:18 +02:00
rest = rest . replace ( " ] " , " " )
2013-01-07 18:20:09 +01:00
if " - " in rest :
( left , right ) = rest . split ( " - " , 1 )
return ( first , ( left , right ) )
else :
return ( first , ( rest , rest ) )
2012-08-11 17:36:59 +02:00
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
2012-12-10 15:54:07 +01:00
pattern = pattern . replace ( " ! " , " " ) . 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 :
2013-05-28 12:53:51 +02:00
if a . name not in groups :
groups [ a . name ] = [ h . name for h in a . get_hosts ( ) ]
2012-09-08 00:07:52 +02:00
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 ) :
2013-05-08 20:11:40 +02:00
if hostname in [ ' localhost ' , ' 127.0.0.1 ' ] :
for host in self . get_group ( ' all ' ) . get_hosts ( ) :
if host . name in [ ' localhost ' , ' 127.0.0.1 ' ] :
2012-05-05 22:31:03 +02:00
return host
2013-05-08 20:11:40 +02:00
else :
for group in self . groups :
for host in group . get_hosts ( ) :
if hostname == host . name :
return host
2012-05-05 22:31:03 +02:00
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 = { }
2013-04-20 15:59:40 +02:00
vars_results = [ plugin . run ( host ) for plugin in self . _vars_plugins ]
for updated in vars_results :
2012-10-29 17:11:57 +01:00
if updated is not None :
vars . update ( updated )
2012-10-27 01:55:59 +02:00
2012-11-27 00:13:56 +01:00
vars . update ( host . get_variables ( ) )
2013-03-04 12:37:15 +01:00
if self . parser is not None :
vars . update ( self . parser . get_host_variables ( host ) )
2012-10-27 01:55:59 +02:00
return vars
2012-05-05 22:31:03 +02:00
def add_group ( self , group ) :
self . groups . append ( group )
2013-03-19 18:57:45 +01:00
self . _groups_list = None # invalidate internal cache
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 ) :
2013-01-23 17:43:24 +01:00
return sorted ( [ g . name for g in self . groups ] , key = lambda x : x )
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 .
"""
2013-05-23 18:33:29 +02:00
if not isinstance ( restriction , list ) :
2012-07-15 15:32:47 +02:00
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 .
"""
2013-05-23 18:33:29 +02:00
if not isinstance ( restriction , list ) :
2012-08-18 15:52:13 +02:00
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 ( ' , ' , ' : ' )
2013-04-08 18:36:01 +02:00
subset_pattern = subset_pattern . replace ( " ; " , " : " ) . split ( " : " )
results = [ ]
# allow Unix style @filename data
for x in subset_pattern :
2013-04-10 22:37:49 +02:00
if x . startswith ( " @ " ) :
fd = open ( x [ 1 : ] )
results . extend ( fd . read ( ) . split ( " \n " ) )
fd . close ( )
else :
results . append ( x )
2013-04-08 18:36:01 +02:00
self . _subset = results
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 )
2013-06-01 16:38:16 +02:00
def playbook_basedir ( self ) :
""" returns the directory of the current playbook """
return self . _playbook_basedir
def set_playbook_basedir ( self , dir ) :
"""
sets the base directory of the playbook so inventory plugins can use it to find
variable files and other things .
"""
self . _playbook_basedir = dir