@ -19,13 +19,15 @@
from __future__ import ( absolute_import , division , print_function )
from __future__ import ( absolute_import , division , print_function )
__metaclass__ = type
__metaclass__ = type
from collections import MutableMapping
import hashlib
import hashlib
import os
import os
import re
import re
import string
import string
from collections import MutableMapping
from ansible . errors import AnsibleError , AnsibleOptionsError , AnsibleParserError
from ansible . errors import AnsibleError , AnsibleOptionsError , AnsibleParserError
from ansible . plugins import AnsiblePlugin
from ansible . module_utils . _text import to_bytes , to_native
from ansible . module_utils . _text import to_bytes , to_native
from ansible . module_utils . parsing . convert_bool import boolean
from ansible . module_utils . parsing . convert_bool import boolean
from ansible . module_utils . six import string_types
from ansible . module_utils . six import string_types
@ -40,16 +42,106 @@ except ImportError:
_SAFE_GROUP = re . compile ( " [^A-Za-z0-9 \ _] " )
_SAFE_GROUP = re . compile ( " [^A-Za-z0-9 \ _] " )
class BaseInventoryPlugin ( object ) :
# Helper methods
def to_safe_group_name ( name ) :
''' Converts ' bad ' characters in a string to underscores so they can be used as Ansible hosts or groups '''
return _SAFE_GROUP . sub ( " _ " , name )
def detect_range ( line = None ) :
'''
A helper function that checks a given host line to see if it contains
a range pattern described in the docstring above .
Returns True if the given line contains a pattern , else False .
'''
return ' [ ' in line
def expand_hostname_range ( line = None ) :
'''
A helper function that expands a given line that contains a pattern
specified in top docstring , and returns a list that consists of the
expanded version .
The ' [ ' and ' ] ' characters are used to maintain the pseudo - code
appearance . They are replaced in this function with ' | ' to ease
string splitting .
References : http : / / ansible . github . com / patterns . html #hosts-and-groups
'''
all_hosts = [ ]
if line :
# A hostname such as db[1:6]-node is considered to consists
# three parts:
# head: 'db'
# nrange: [1:6]; range() is a built-in. Can't use the name
# tail: '-node'
# Add support for multiple ranges in a host so:
# db[01:10:3]node-[01:10]
# - to do this we split off at the first [...] set, getting the list
# of hosts and then repeat until none left.
# - also add an optional third parameter which contains the step. (Default: 1)
# so range can be [01:10:2] -> 01 03 05 07 09
( head , nrange , tail ) = line . replace ( ' [ ' , ' | ' , 1 ) . replace ( ' ] ' , ' | ' , 1 ) . split ( ' | ' )
bounds = nrange . split ( " : " )
if len ( bounds ) != 2 and len ( bounds ) != 3 :
raise AnsibleError ( " host range must be begin:end or begin:end:step " )
beg = bounds [ 0 ]
end = bounds [ 1 ]
if len ( bounds ) == 2 :
step = 1
else :
step = bounds [ 2 ]
if not beg :
beg = " 0 "
if not end :
raise AnsibleError ( " host range must specify end value " )
if beg [ 0 ] == ' 0 ' and len ( beg ) > 1 :
rlen = len ( beg ) # range length formatting hint
if rlen != len ( end ) :
raise AnsibleError ( " host range must specify equal-length begin and end formats " )
def fill ( x ) :
return str ( x ) . zfill ( rlen ) # range sequence
else :
fill = str
try :
i_beg = string . ascii_letters . index ( beg )
i_end = string . ascii_letters . index ( end )
if i_beg > i_end :
raise AnsibleError ( " host range must have begin <= end " )
seq = list ( string . ascii_letters [ i_beg : i_end + 1 : int ( step ) ] )
except ValueError : # not an alpha range
seq = range ( int ( beg ) , int ( end ) + 1 , int ( step ) )
for rseq in seq :
hname = ' ' . join ( ( head , fill ( rseq ) , tail ) )
if detect_range ( hname ) :
all_hosts . extend ( expand_hostname_range ( hname ) )
else :
all_hosts . append ( hname )
return all_hosts
class BaseInventoryPlugin ( AnsiblePlugin ) :
""" Parses an Inventory Source """
""" Parses an Inventory Source """
TYPE = ' generator '
TYPE = ' generator '
def __init__ ( self ) :
def __init__ ( self ) :
super ( BaseInventoryPlugin , self ) . __init__ ( )
self . _options = { }
self . inventory = None
self . inventory = None
self . display = display
self . display = display
self . _cache = { }
def parse ( self , inventory , loader , path , cache = True ) :
def parse ( self , inventory , loader , path , cache = True ) :
''' Populates self.groups from the given data. Raises an error on any parse failure. '''
''' Populates self.groups from the given data. Raises an error on any parse failure. '''
@ -64,7 +156,64 @@ class BaseInventoryPlugin(object):
b_path = to_bytes ( path , errors = ' surrogate_or_strict ' )
b_path = to_bytes ( path , errors = ' surrogate_or_strict ' )
return ( os . path . exists ( b_path ) and os . access ( b_path , os . R_OK ) )
return ( os . path . exists ( b_path ) and os . access ( b_path , os . R_OK ) )
def get_cache_prefix ( self , path ) :
def _populate_host_vars ( self , hosts , variables , group = None , port = None ) :
if not isinstance ( variables , MutableMapping ) :
raise AnsibleParserError ( " Invalid data from file, expected dictionary and got: \n \n %s " % to_native ( variables ) )
for host in hosts :
self . inventory . add_host ( host , group = group , port = port )
for k in variables :
self . inventory . set_variable ( host , k , variables [ k ] )
def _read_config_data ( self , path ) :
''' validate config and set options as appropriate '''
config = { }
try :
config = self . loader . load_from_file ( path )
except Exception as e :
raise AnsibleParserError ( to_native ( e ) )
if not config :
# no data
raise AnsibleParserError ( " %s is empty " % ( to_native ( path ) ) )
elif config . get ( ' plugin ' ) != self . NAME :
# this is not my config file
raise AnsibleParserError ( " Incorrect plugin name in file: %s " % config . get ( ' plugin ' , ' none found ' ) )
elif not isinstance ( config , MutableMapping ) :
# configs are dictionaries
raise AnsibleParserError ( ' inventory source has invalid structure, it should be a dictionary, got: %s ' % type ( config ) )
self . set_options ( direct = config )
return config
def _consume_options ( self , data ) :
''' update existing options from file data '''
for k in self . _options :
if k in data :
self . _options [ k ] = data . pop ( k )
def clear_cache ( self ) :
pass
class BaseFileInventoryPlugin ( BaseInventoryPlugin ) :
""" Parses a File based Inventory Source """
TYPE = ' storage '
def __init__ ( self ) :
super ( BaseFileInventoryPlugin , self ) . __init__ ( )
class Cacheable ( object ) :
_cache = { }
def _get_cache_prefix ( self , path ) :
''' create predictable unique prefix for plugin/inventory '''
''' create predictable unique prefix for plugin/inventory '''
m = hashlib . sha1 ( )
m = hashlib . sha1 ( )
@ -78,16 +227,10 @@ class BaseInventoryPlugin(object):
return ' s_ ' . join ( [ d1 [ : 5 ] , d2 [ : 5 ] ] )
return ' s_ ' . join ( [ d1 [ : 5 ] , d2 [ : 5 ] ] )
def clear_cache ( self ) :
def clear_cache ( self ) :
pass
self . _cache = { }
def populate_host_vars ( self , hosts , variables , group = None , port = None ) :
if not isinstance ( variables , MutableMapping ) :
raise AnsibleParserError ( " Invalid data from file, expected dictionary and got: \n \n %s " % to_native ( variables ) )
for host in hosts :
class Constructable ( object ) :
self . inventory . add_host ( host , group = group , port = port )
for k in variables :
self . inventory . set_variable ( host , k , variables [ k ] )
def _compose ( self , template , variables ) :
def _compose ( self , template , variables ) :
''' helper method for pluigns to compose variables for Ansible based on jinja2 expression and inventory vars '''
''' helper method for pluigns to compose variables for Ansible based on jinja2 expression and inventory vars '''
@ -153,101 +296,3 @@ class BaseInventoryPlugin(object):
raise AnsibleOptionsError ( " No key supplied, invalid entry " )
raise AnsibleOptionsError ( " No key supplied, invalid entry " )
else :
else :
raise AnsibleOptionsError ( " Invalid keyed group entry, it must be a dictionary: %s " % keyed )
raise AnsibleOptionsError ( " Invalid keyed group entry, it must be a dictionary: %s " % keyed )
class BaseFileInventoryPlugin ( BaseInventoryPlugin ) :
""" Parses a File based Inventory Source """
TYPE = ' storage '
def __init__ ( self ) :
super ( BaseFileInventoryPlugin , self ) . __init__ ( )
# Helper methods
def to_safe_group_name ( name ) :
''' Converts ' bad ' characters in a string to underscores so they can be used as Ansible hosts or groups '''
return _SAFE_GROUP . sub ( " _ " , name )
def detect_range ( line = None ) :
'''
A helper function that checks a given host line to see if it contains
a range pattern described in the docstring above .
Returns True if the given line contains a pattern , else False .
'''
return ' [ ' in line
def expand_hostname_range ( line = None ) :
'''
A helper function that expands a given line that contains a pattern
specified in top docstring , and returns a list that consists of the
expanded version .
The ' [ ' and ' ] ' characters are used to maintain the pseudo - code
appearance . They are replaced in this function with ' | ' to ease
string splitting .
References : http : / / ansible . github . com / patterns . html #hosts-and-groups
'''
all_hosts = [ ]
if line :
# A hostname such as db[1:6]-node is considered to consists
# three parts:
# head: 'db'
# nrange: [1:6]; range() is a built-in. Can't use the name
# tail: '-node'
# Add support for multiple ranges in a host so:
# db[01:10:3]node-[01:10]
# - to do this we split off at the first [...] set, getting the list
# of hosts and then repeat until none left.
# - also add an optional third parameter which contains the step. (Default: 1)
# so range can be [01:10:2] -> 01 03 05 07 09
( head , nrange , tail ) = line . replace ( ' [ ' , ' | ' , 1 ) . replace ( ' ] ' , ' | ' , 1 ) . split ( ' | ' )
bounds = nrange . split ( " : " )
if len ( bounds ) != 2 and len ( bounds ) != 3 :
raise AnsibleError ( " host range must be begin:end or begin:end:step " )
beg = bounds [ 0 ]
end = bounds [ 1 ]
if len ( bounds ) == 2 :
step = 1
else :
step = bounds [ 2 ]
if not beg :
beg = " 0 "
if not end :
raise AnsibleError ( " host range must specify end value " )
if beg [ 0 ] == ' 0 ' and len ( beg ) > 1 :
rlen = len ( beg ) # range length formatting hint
if rlen != len ( end ) :
raise AnsibleError ( " host range must specify equal-length begin and end formats " )
def fill ( x ) :
return str ( x ) . zfill ( rlen ) # range sequence
else :
fill = str
try :
i_beg = string . ascii_letters . index ( beg )
i_end = string . ascii_letters . index ( end )
if i_beg > i_end :
raise AnsibleError ( " host range must have begin <= end " )
seq = list ( string . ascii_letters [ i_beg : i_end + 1 : int ( step ) ] )
except ValueError : # not an alpha range
seq = range ( int ( beg ) , int ( end ) + 1 , int ( step ) )
for rseq in seq :
hname = ' ' . join ( ( head , fill ( rseq ) , tail ) )
if detect_range ( hname ) :
all_hosts . extend ( expand_hostname_range ( hname ) )
else :
all_hosts . append ( hname )
return all_hosts