2015-10-26 21:23:09 +00:00
# (c) 2012-2014, 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/>.
# Make coding more python3-ish
from __future__ import ( absolute_import , division , print_function )
__metaclass__ = type
import copy
import os
2016-03-03 23:13:36 +00:00
import json
2015-10-26 21:23:09 +00:00
import subprocess
2016-01-22 16:23:10 +00:00
import tempfile
2016-04-14 14:31:39 +00:00
from yaml import YAMLError
2016-01-22 16:23:10 +00:00
2015-10-26 21:23:09 +00:00
from ansible . compat . six import text_type , string_types
from ansible . errors import AnsibleFileNotFound , AnsibleParserError , AnsibleError
from ansible . errors . yaml_strings import YAML_SYNTAX_ERROR
from ansible . parsing . vault import VaultLib
from ansible . parsing . quoting import unquote
from ansible . parsing . yaml . loader import AnsibleLoader
from ansible . parsing . yaml . objects import AnsibleBaseYAMLObject , AnsibleUnicode
from ansible . module_utils . basic import is_executable
from ansible . utils . path import unfrackpath
2016-03-03 17:03:28 +00:00
from ansible . utils . unicode import to_unicode , to_bytes
2015-10-26 21:23:09 +00:00
2016-06-28 21:23:30 +00:00
try :
from __main__ import display
except ImportError :
from ansible . utils . display import Display
display = Display ( )
2015-10-26 21:23:09 +00:00
class DataLoader ( ) :
'''
The DataLoader class is used to load and parse YAML or JSON content ,
either from a given file name or from a string that was previously
read in through other means . A Vault password can be specified , and
any vault - encrypted files will be decrypted .
Data read from files will also be cached , so the file will never be
read from disk more than once .
Usage :
dl = DataLoader ( )
2015-11-18 10:20:34 +00:00
# optionally: dl.set_vault_password('foo')
2015-10-26 21:23:09 +00:00
ds = dl . load ( ' ... ' )
ds = dl . load_from_file ( ' /path/to/file ' )
'''
def __init__ ( self ) :
self . _basedir = ' . '
self . _FILE_CACHE = dict ( )
2016-01-22 16:23:10 +00:00
self . _tempfiles = set ( )
2015-10-26 21:23:09 +00:00
# initialize the vault stuff with an empty password
self . set_vault_password ( None )
def set_vault_password ( self , vault_password ) :
self . _vault_password = vault_password
self . _vault = VaultLib ( password = vault_password )
def load ( self , data , file_name = ' <string> ' , show_content = True ) :
'''
Creates a python datastructure from the given data , which can be either
a JSON or YAML string .
'''
2016-03-03 18:25:47 +00:00
new_data = None
2015-10-26 21:23:09 +00:00
try :
2016-03-03 18:25:47 +00:00
# we first try to load this data as JSON
new_data = json . loads ( data )
except :
# must not be JSON, let the rest try
if isinstance ( data , AnsibleUnicode ) :
# The PyYAML's libyaml bindings use PyUnicode_CheckExact so
# they are unable to cope with our subclass.
# Unwrap and re-wrap the unicode so we can keep track of line
# numbers
in_data = text_type ( data )
else :
in_data = data
try :
new_data = self . _safe_load ( in_data , file_name = file_name )
except YAMLError as yaml_exc :
self . _handle_error ( yaml_exc , file_name , show_content )
if isinstance ( data , AnsibleUnicode ) :
new_data = AnsibleUnicode ( new_data )
new_data . ansible_pos = data . ansible_pos
2016-01-29 00:43:17 +00:00
return new_data
2015-10-26 21:23:09 +00:00
def load_from_file ( self , file_name ) :
''' Loads data from a file, which can contain either JSON or YAML. '''
file_name = self . path_dwim ( file_name )
# if the file has already been read in and cached, we'll
# return those results to avoid more file/vault operations
if file_name in self . _FILE_CACHE :
parsed_data = self . _FILE_CACHE [ file_name ]
else :
# read the file contents and load the data structure from them
( file_data , show_content ) = self . _get_file_contents ( file_name )
parsed_data = self . load ( data = file_data , file_name = file_name , show_content = show_content )
# cache the file contents for next time
self . _FILE_CACHE [ file_name ] = parsed_data
# return a deep copy here, so the cache is not affected
return copy . deepcopy ( parsed_data )
def path_exists ( self , path ) :
path = self . path_dwim ( path )
2016-03-04 10:11:35 +00:00
return os . path . exists ( to_bytes ( path , errors = ' strict ' ) )
2015-10-26 21:23:09 +00:00
def is_file ( self , path ) :
path = self . path_dwim ( path )
2016-03-03 17:03:28 +00:00
return os . path . isfile ( to_bytes ( path , errors = ' strict ' ) ) or path == os . devnull
2015-10-26 21:23:09 +00:00
def is_directory ( self , path ) :
path = self . path_dwim ( path )
2016-03-03 17:03:28 +00:00
return os . path . isdir ( to_bytes ( path , errors = ' strict ' ) )
2015-10-26 21:23:09 +00:00
def list_directory ( self , path ) :
path = self . path_dwim ( path )
return os . listdir ( path )
def is_executable ( self , path ) :
''' is the given path executable? '''
path = self . path_dwim ( path )
return is_executable ( path )
def _safe_load ( self , stream , file_name = None ) :
''' Implements yaml.safe_load(), except using our custom loader class. '''
loader = AnsibleLoader ( stream , file_name )
try :
return loader . get_single_data ( )
finally :
2016-03-03 23:13:36 +00:00
try :
loader . dispose ( )
except AttributeError :
pass # older versions of yaml don't have dispose function, ignore
2015-10-26 21:23:09 +00:00
def _get_file_contents ( self , file_name ) :
'''
Reads the file contents from the given file name , and will decrypt them
if they are found to be vault - encrypted .
'''
if not file_name or not isinstance ( file_name , string_types ) :
raise AnsibleParserError ( " Invalid filename: ' %s ' " % str ( file_name ) )
2016-04-30 05:19:12 +00:00
b_file_name = to_bytes ( file_name )
if not self . path_exists ( b_file_name ) or not self . is_file ( b_file_name ) :
2015-10-26 21:23:09 +00:00
raise AnsibleFileNotFound ( " the file_name ' %s ' does not exist, or is not readable " % file_name )
show_content = True
try :
2016-04-30 05:19:12 +00:00
with open ( b_file_name , ' rb ' ) as f :
2015-10-26 21:23:09 +00:00
data = f . read ( )
if self . _vault . is_encrypted ( data ) :
2016-06-18 13:30:08 +00:00
data = self . _vault . decrypt ( data , filename = b_file_name )
2015-10-26 21:23:09 +00:00
show_content = False
data = to_unicode ( data , errors = ' strict ' )
return ( data , show_content )
except ( IOError , OSError ) as e :
raise AnsibleParserError ( " an error occurred while trying to read the file ' %s ' : %s " % ( file_name , str ( e ) ) )
def _handle_error ( self , yaml_exc , file_name , show_content ) :
'''
Optionally constructs an object ( AnsibleBaseYAMLObject ) to encapsulate the
file name / position where a YAML exception occurred , and raises an AnsibleParserError
to display the syntax exception information .
'''
# if the YAML exception contains a problem mark, use it to construct
# an object the error class can use to display the faulty line
err_obj = None
if hasattr ( yaml_exc , ' problem_mark ' ) :
err_obj = AnsibleBaseYAMLObject ( )
err_obj . ansible_pos = ( file_name , yaml_exc . problem_mark . line + 1 , yaml_exc . problem_mark . column + 1 )
raise AnsibleParserError ( YAML_SYNTAX_ERROR , obj = err_obj , show_content = show_content )
def get_basedir ( self ) :
''' returns the current basedir '''
return self . _basedir
def set_basedir ( self , basedir ) :
''' sets the base directory, used to find files when a relative path is given '''
if basedir is not None :
self . _basedir = to_unicode ( basedir )
def path_dwim ( self , given ) :
'''
make relative paths work like folks expect .
'''
given = unquote ( given )
2016-02-26 18:26:49 +00:00
given = to_unicode ( given , errors = ' strict ' )
2015-10-26 21:23:09 +00:00
2016-02-26 18:26:49 +00:00
if given . startswith ( u " / " ) :
2015-10-26 21:23:09 +00:00
return os . path . abspath ( given )
2016-02-26 18:26:49 +00:00
elif given . startswith ( u " ~ " ) :
2015-10-26 21:23:09 +00:00
return os . path . abspath ( os . path . expanduser ( given ) )
else :
2016-02-26 18:26:49 +00:00
basedir = to_unicode ( self . _basedir , errors = ' strict ' )
return os . path . abspath ( os . path . join ( basedir , given ) )
2015-10-26 21:23:09 +00:00
def path_dwim_relative ( self , path , dirname , source ) :
'''
find one file in either a role or playbook dir with or without
explicitly named dirname subdirs
Used in action plugins and lookups to find supplemental files that
could be in either place .
'''
search = [ ]
isrole = False
# I have full path, nothing else needs to be looked at
2016-06-28 21:23:30 +00:00
if source . startswith ( ' ~ ' ) or source . startswith ( os . path . sep ) :
2015-10-26 21:23:09 +00:00
search . append ( self . path_dwim ( source ) )
else :
# base role/play path + templates/files/vars + relative filename
search . append ( os . path . join ( path , dirname , source ) )
basedir = unfrackpath ( path )
# is it a role and if so make sure you get correct base path
2016-03-03 17:03:28 +00:00
if path . endswith ( ' tasks ' ) and os . path . exists ( to_bytes ( os . path . join ( path , ' main.yml ' ) , errors = ' strict ' ) ) \
or os . path . exists ( to_bytes ( os . path . join ( path , ' tasks/main.yml ' ) , errors = ' strict ' ) ) :
2015-10-26 21:23:09 +00:00
isrole = True
if path . endswith ( ' tasks ' ) :
basedir = unfrackpath ( os . path . dirname ( path ) )
cur_basedir = self . _basedir
self . set_basedir ( basedir )
# resolved base role/play path + templates/files/vars + relative filename
search . append ( self . path_dwim ( os . path . join ( basedir , dirname , source ) ) )
self . set_basedir ( cur_basedir )
if isrole and not source . endswith ( dirname ) :
# look in role's tasks dir w/o dirname
search . append ( self . path_dwim ( os . path . join ( basedir , ' tasks ' , source ) ) )
# try to create absolute path for loader basedir + templates/files/vars + filename
search . append ( self . path_dwim ( os . path . join ( dirname , source ) ) )
search . append ( self . path_dwim ( os . path . join ( basedir , source ) ) )
# try to create absolute path for loader basedir + filename
search . append ( self . path_dwim ( source ) )
for candidate in search :
2016-03-03 17:03:28 +00:00
if os . path . exists ( to_bytes ( candidate , errors = ' strict ' ) ) :
2015-10-26 21:23:09 +00:00
break
return candidate
2016-06-28 21:23:30 +00:00
def path_dwim_relative_stack ( self , paths , dirname , source ) :
'''
find one file in first path in stack taking roles into account and adding play basedir as fallback
'''
result = None
if source . startswith ( ' ~ ' ) or source . startswith ( os . path . sep ) :
# path is absolute, no relative needed, check existence and return source
test_path = to_bytes ( unfrackpath ( source ) , errors = ' strict ' )
if os . path . exists ( test_path ) :
result = test_path
else :
search = [ ]
for path in paths :
upath = unfrackpath ( path )
mydir = os . path . dirname ( upath )
# if path is in role and 'tasks' not there already, add it into the search
if upath . endswith ( ' tasks ' ) and os . path . exists ( to_bytes ( os . path . join ( upath , ' main.yml ' ) , errors = ' strict ' ) ) \
or os . path . exists ( to_bytes ( os . path . join ( upath , ' tasks/main.yml ' ) , errors = ' strict ' ) ) \
or os . path . exists ( to_bytes ( os . path . join ( os . path . dirname ( upath ) , ' tasks/main.yml ' ) , errors = ' strict ' ) ) :
if mydir . endswith ( ' tasks ' ) :
search . append ( os . path . join ( os . path . dirname ( mydir ) , dirname , source ) )
search . append ( os . path . join ( mydir , source ) )
else :
search . append ( os . path . join ( upath , dirname , source ) )
search . append ( os . path . join ( upath , ' tasks ' , source ) )
elif dirname not in source . split ( ' / ' ) :
# don't add dirname if user already is using it in source
search . append ( os . path . join ( upath , dirname , source ) )
search . append ( os . path . join ( upath , source ) )
# always append basedir as last resort
search . append ( os . path . join ( self . get_basedir ( ) , dirname , source ) )
search . append ( os . path . join ( self . get_basedir ( ) , source ) )
display . debug ( ' search_path: \n \t ' + ' \n \t ' . join ( search ) )
for candidate in search :
display . vvvvv ( ' looking for " %s " at " %s " ' % ( source , candidate ) )
if os . path . exists ( to_bytes ( candidate , errors = ' strict ' ) ) :
result = candidate
break
return result
2015-10-26 21:23:09 +00:00
def read_vault_password_file ( self , vault_password_file ) :
"""
Read a vault password from a file or if executable , execute the script and
retrieve password from STDOUT
"""
2016-03-03 17:03:28 +00:00
this_path = os . path . realpath ( to_bytes ( os . path . expanduser ( vault_password_file ) , errors = ' strict ' ) )
if not os . path . exists ( to_bytes ( this_path , errors = ' strict ' ) ) :
2015-10-26 21:23:09 +00:00
raise AnsibleFileNotFound ( " The vault password file %s was not found " % this_path )
if self . is_executable ( this_path ) :
try :
# STDERR not captured to make it easier for users to prompt for input in their scripts
p = subprocess . Popen ( this_path , stdout = subprocess . PIPE )
except OSError as e :
raise AnsibleError ( " Problem running vault password script %s ( %s ). If this is not a script, remove the executable bit from the file. " % ( ' ' . join ( this_path ) , e ) )
stdout , stderr = p . communicate ( )
self . set_vault_password ( stdout . strip ( ' \r \n ' ) )
else :
try :
f = open ( this_path , " rb " )
self . set_vault_password ( f . read ( ) . strip ( ) )
f . close ( )
except ( OSError , IOError ) as e :
raise AnsibleError ( " Could not read vault password file %s : %s " % ( this_path , e ) )
2016-01-22 16:23:10 +00:00
def _create_content_tempfile ( self , content ) :
''' Create a tempfile containing defined content '''
fd , content_tempfile = tempfile . mkstemp ( )
f = os . fdopen ( fd , ' wb ' )
content = to_bytes ( content )
try :
f . write ( content )
except Exception as err :
os . remove ( content_tempfile )
raise Exception ( err )
finally :
f . close ( )
return content_tempfile
def get_real_file ( self , file_path ) :
"""
If the file is vault encrypted return a path to a temporary decrypted file
If the file is not encrypted then the path is returned
Temporary files are cleanup in the destructor
"""
if not file_path or not isinstance ( file_path , string_types ) :
raise AnsibleParserError ( " Invalid filename: ' %s ' " % str ( file_path ) )
if not self . path_exists ( file_path ) or not self . is_file ( file_path ) :
raise AnsibleFileNotFound ( " the file_name ' %s ' does not exist, or is not readable " % file_path )
if not self . _vault :
self . _vault = VaultLib ( password = " " )
real_path = self . path_dwim ( file_path )
try :
2016-04-19 15:32:08 +00:00
with open ( to_bytes ( real_path ) , ' rb ' ) as f :
2016-01-22 16:23:10 +00:00
data = f . read ( )
if self . _vault . is_encrypted ( data ) :
# if the file is encrypted and no password was specified,
# the decrypt call would throw an error, but we check first
# since the decrypt function doesn't know the file name
if not self . _vault_password :
raise AnsibleParserError ( " A vault password must be specified to decrypt %s " % file_path )
2016-06-18 13:30:08 +00:00
data = self . _vault . decrypt ( data , filename = real_path )
2016-01-22 16:23:10 +00:00
# Make a temp file
real_path = self . _create_content_tempfile ( data )
self . _tempfiles . add ( real_path )
return real_path
except ( IOError , OSError ) as e :
2016-04-14 14:31:39 +00:00
raise AnsibleParserError ( " an error occurred while trying to read the file ' %s ' : %s " % ( real_path , str ( e ) ) )
2016-01-22 16:23:10 +00:00
2016-04-14 14:31:39 +00:00
def cleanup_tmp_file ( self , file_path ) :
2016-01-22 16:23:10 +00:00
"""
Removes any temporary files created from a previous call to
get_real_file . file_path must be the path returned from a
previous call to get_real_file .
"""
if file_path in self . _tempfiles :
os . unlink ( file_path )
self . _tempfiles . remove ( file_path ) ;
2016-04-14 14:31:39 +00:00
def cleanup_all_tmp_files ( self ) :
for f in self . _tempfiles :
try :
self . cleanup_tmp_file ( f )
except :
pass #TODO: this should at least warn