2014-01-04 18:31:44 +00:00
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
2012-05-06 22:02:53 +00:00
#
# 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/>.
#############################################
2015-05-04 02:47:26 +00:00
from __future__ import ( absolute_import , division , print_function )
__metaclass__ = type
2012-05-06 22:02:53 +00:00
2013-03-17 18:50:15 +00:00
import os
2012-05-06 22:02:53 +00:00
import subprocess
2015-05-04 02:47:26 +00:00
import sys
2015-06-03 18:26:53 +00:00
from collections import Mapping
2015-10-16 00:55:23 +00:00
from ansible . compat . six import iteritems
2015-09-03 06:23:27 +00:00
2015-05-04 02:47:26 +00:00
from ansible import constants as C
2015-10-19 18:13:56 +00:00
from ansible . errors import AnsibleError
2012-05-25 23:34:13 +00:00
from ansible . inventory . host import Host
from ansible . inventory . group import Group
2015-01-27 06:01:52 +00:00
from ansible . module_utils . basic import json_dict_bytes_to_unicode
2016-01-19 16:56:07 +00:00
from ansible . utils . unicode import to_str , to_unicode
2012-05-06 22:02:53 +00:00
2014-10-03 00:26:09 +00:00
2015-05-04 02:47:26 +00:00
class InventoryScript :
2012-07-15 16:29:53 +00:00
''' Host inventory parser for ansible using external inventory scripts. '''
2012-05-06 22:02:53 +00:00
2015-09-16 19:49:45 +00:00
def __init__ ( self , loader , groups = None , filename = C . DEFAULT_HOST_LIST ) :
if groups is None :
groups = dict ( )
2015-05-04 02:47:26 +00:00
self . _loader = loader
2015-09-16 15:27:03 +00:00
self . groups = groups
2012-05-06 22:02:53 +00:00
2013-03-17 18:50:15 +00:00
# Support inventory scripts that are not prefixed with some
# path information but happen to be in the current working
# directory when '.' is not in PATH.
self . filename = os . path . abspath ( filename )
2013-02-27 19:35:08 +00:00
cmd = [ self . filename , " --list " ]
2012-11-06 15:39:10 +00:00
try :
sp = subprocess . Popen ( cmd , stdout = subprocess . PIPE , stderr = subprocess . PIPE )
2015-08-27 06:16:11 +00:00
except OSError as e :
2015-05-04 02:47:26 +00:00
raise AnsibleError ( " problem running %s ( %s ) " % ( ' ' . join ( cmd ) , e ) )
2012-05-06 22:02:53 +00:00
( stdout , stderr ) = sp . communicate ( )
2015-01-15 16:52:22 +00:00
if sp . returncode != 0 :
2015-05-04 02:47:26 +00:00
raise AnsibleError ( " Inventory script ( %s ) had an execution error: %s " % ( filename , stderr ) )
2015-01-15 16:52:22 +00:00
2016-01-19 16:56:07 +00:00
# make sure script output is unicode so that json loader will output
# unicode strings itself
try :
self . data = to_unicode ( stdout , errors = " strict " )
except Exception as e :
raise AnsibleError ( " inventory data from {0} contained characters that cannot be interpreted as UTF-8: {1} " . format ( to_str ( self . filename ) , to_str ( e ) ) )
2013-08-04 15:34:42 +00:00
# see comment about _meta below
self . host_vars_from_top = None
2015-09-16 15:27:03 +00:00
self . _parse ( stderr )
2012-05-06 22:02:53 +00:00
2013-04-18 02:27:00 +00:00
def _parse ( self , err ) :
2013-02-09 16:37:55 +00:00
2012-10-10 16:35:45 +00:00
all_hosts = { }
2014-07-03 20:11:14 +00:00
# not passing from_remote because data from CMDB is trusted
2015-06-03 18:26:53 +00:00
try :
self . raw = self . _loader . load ( self . data )
except Exception as e :
sys . stderr . write ( err + " \n " )
2016-01-06 23:46:42 +00:00
raise AnsibleError ( " failed to parse executable inventory script results from {0} : {1} " . format ( to_str ( self . filename ) , to_str ( e ) ) )
2015-06-03 18:26:53 +00:00
if not isinstance ( self . raw , Mapping ) :
sys . stderr . write ( err + " \n " )
2016-01-06 23:18:22 +00:00
raise AnsibleError ( " failed to parse executable inventory script results from {0} : data needs to be formatted as a json dict " . format ( to_str ( self . filename ) ) )
2015-06-03 18:26:53 +00:00
2015-09-16 15:27:03 +00:00
group = None
2012-11-26 23:13:56 +00:00
for ( group_name , data ) in self . raw . items ( ) :
2014-03-10 11:49:50 +00:00
2013-08-04 15:34:42 +00:00
# in Ansible 1.3 and later, a "_meta" subelement may contain
# a variable "hostvars" which contains a hash for each host
# if this "hostvars" exists at all then do not call --host for each
# host. This is for efficiency and scripts should still return data
# if called with --host for backwards compat with 1.2 and earlier.
if group_name == ' _meta ' :
if ' hostvars ' in data :
self . host_vars_from_top = data [ ' hostvars ' ]
continue
2013-02-09 16:37:55 +00:00
2015-09-16 15:27:03 +00:00
if group_name not in self . groups :
group = self . groups [ group_name ] = Group ( group_name )
group = self . groups [ group_name ]
2012-05-06 22:02:53 +00:00
host = None
2013-02-09 16:37:55 +00:00
2012-11-26 23:13:56 +00:00
if not isinstance ( data , dict ) :
data = { ' hosts ' : data }
2014-08-21 15:53:11 +00:00
# is not those subkeys, then simplified syntax, host with vars
2016-01-08 12:52:44 +00:00
elif not any ( k in data for k in ( ' hosts ' , ' vars ' , ' children ' ) ) :
2013-05-08 17:55:50 +00:00
data = { ' hosts ' : [ group_name ] , ' vars ' : data }
2013-02-09 16:37:55 +00:00
2012-11-26 23:13:56 +00:00
if ' hosts ' in data :
2014-08-21 15:53:11 +00:00
if not isinstance ( data [ ' hosts ' ] , list ) :
2015-05-04 02:47:26 +00:00
raise AnsibleError ( " You defined a group \" %s \" with bad "
2014-08-21 15:53:11 +00:00
" data for the host list: \n %s " % ( group_name , data ) )
2013-02-09 16:37:55 +00:00
2012-11-26 23:13:56 +00:00
for hostname in data [ ' hosts ' ] :
2016-01-06 23:46:42 +00:00
if hostname not in all_hosts :
2012-11-26 23:13:56 +00:00
all_hosts [ hostname ] = Host ( hostname )
host = all_hosts [ hostname ]
group . add_host ( host )
2013-02-09 16:37:55 +00:00
2012-11-26 23:13:56 +00:00
if ' vars ' in data :
2014-08-21 15:53:11 +00:00
if not isinstance ( data [ ' vars ' ] , dict ) :
2015-05-04 02:47:26 +00:00
raise AnsibleError ( " You defined a group \" %s \" with bad "
2014-08-21 15:53:11 +00:00
" data for variables: \n %s " % ( group_name , data ) )
2015-09-03 06:23:27 +00:00
for k , v in iteritems ( data [ ' vars ' ] ) :
2015-09-16 15:27:03 +00:00
group . set_variable ( k , v )
2013-02-09 16:37:55 +00:00
2012-11-26 23:13:56 +00:00
# Separate loop to ensure all groups are defined
for ( group_name , data ) in self . raw . items ( ) :
2013-08-04 15:34:42 +00:00
if group_name == ' _meta ' :
continue
2012-11-26 23:13:56 +00:00
if isinstance ( data , dict ) and ' children ' in data :
for child_name in data [ ' children ' ] :
2015-09-16 15:27:03 +00:00
if child_name in self . groups :
self . groups [ group_name ] . add_child_group ( self . groups [ child_name ] )
2014-03-10 11:49:50 +00:00
2015-09-16 15:27:03 +00:00
# Finally, add all top-level groups as children of 'all'.
# We exclude ungrouped here because it was already added as a child of
# 'all' at the time it was created.
2014-03-10 11:49:50 +00:00
2015-09-16 15:27:03 +00:00
for group in self . groups . values ( ) :
if group . depth == 0 and group . name not in ( ' all ' , ' ungrouped ' ) :
self . groups [ ' all ' ] . add_child_group ( group )
2013-02-27 19:35:08 +00:00
def get_host_variables ( self , host ) :
""" Runs <script> --host <hostname> to determine additional host variables """
2013-08-04 15:34:42 +00:00
if self . host_vars_from_top is not None :
2016-01-25 15:49:54 +00:00
try :
got = self . host_vars_from_top . get ( host . name , { } )
except AttributeError as e :
raise AnsibleError ( " Improperly formated host information for %s : %s " % ( host . name , to_str ( e ) ) )
2013-08-04 15:34:42 +00:00
return got
2013-02-27 19:35:08 +00:00
cmd = [ self . filename , " --host " , host . name ]
try :
sp = subprocess . Popen ( cmd , stdout = subprocess . PIPE , stderr = subprocess . PIPE )
2015-08-27 06:16:11 +00:00
except OSError as e :
2015-05-04 02:47:26 +00:00
raise AnsibleError ( " problem running %s ( %s ) " % ( ' ' . join ( cmd ) , e ) )
2013-02-27 19:35:08 +00:00
( out , err ) = sp . communicate ( )
2014-09-11 16:26:54 +00:00
if out . strip ( ) == ' ' :
return dict ( )
try :
2015-05-04 02:47:26 +00:00
return json_dict_bytes_to_unicode ( self . _loader . load ( out ) )
2014-09-11 16:26:54 +00:00
except ValueError :
2015-05-04 02:47:26 +00:00
raise AnsibleError ( " could not parse post variable response: %s , %s " % ( cmd , out ) )