2014-10-02 17:07:05 +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/>.
2014-10-15 23:22:54 +00:00
# Make coding more python3-ish
from __future__ import ( absolute_import , division , print_function )
__metaclass__ = type
2015-06-27 19:18:18 +00:00
import itertools
2015-08-05 17:54:00 +00:00
import operator
2014-11-14 22:14:08 +00:00
import uuid
2015-03-23 20:19:13 +00:00
from functools import partial
2014-10-19 05:14:30 +00:00
from inspect import getmembers
2014-10-15 22:08:28 +00:00
from io import FileIO
2015-09-04 05:39:08 +00:00
from six import iteritems , string_types , text_type
2014-10-15 22:08:28 +00:00
2014-11-14 22:14:08 +00:00
from jinja2 . exceptions import UndefinedError
2014-10-30 21:04:34 +00:00
from ansible . errors import AnsibleParserError
2014-11-14 22:14:08 +00:00
from ansible . parsing import DataLoader
2014-10-03 20:37:32 +00:00
from ansible . playbook . attribute import Attribute , FieldAttribute
2014-11-14 22:14:08 +00:00
from ansible . template import Templar
from ansible . utils . boolean import boolean
from ansible . utils . debug import debug
2015-08-28 15:35:43 +00:00
from ansible . utils . vars import combine_vars , isidentifier
2014-11-14 22:14:08 +00:00
from ansible . template import template
2014-10-03 12:08:03 +00:00
2014-10-15 23:37:29 +00:00
class Base :
2014-10-02 17:07:05 +00:00
2015-04-16 12:36:59 +00:00
# connection/transport
_connection = FieldAttribute ( isa = ' string ' )
_port = FieldAttribute ( isa = ' int ' )
_remote_user = FieldAttribute ( isa = ' string ' )
2015-08-07 04:05:42 +00:00
# variables
2015-08-28 19:54:20 +00:00
_vars = FieldAttribute ( isa = ' dict ' , default = dict ( ) , priority = 100 )
2015-08-07 04:05:42 +00:00
2015-07-29 13:45:08 +00:00
# flags and misc. settings
2015-08-25 14:14:28 +00:00
_environment = FieldAttribute ( isa = ' list ' )
_no_log = FieldAttribute ( isa = ' bool ' )
2015-04-16 12:36:59 +00:00
2015-09-10 02:15:09 +00:00
# param names which have been deprecated/removed
DEPRECATED_ATTRIBUTES = [
' sudo ' , ' sudo_user ' , ' sudo_pass ' , ' sudo_exe ' , ' sudo_flags ' ,
' su ' , ' su_user ' , ' su_pass ' , ' su_exe ' , ' su_flags ' ,
]
2014-10-28 19:35:29 +00:00
def __init__ ( self ) :
2014-10-19 05:14:30 +00:00
2014-11-14 22:14:08 +00:00
# initialize the data loader and variable manager, which will be provided
# later when the object is actually loaded
2014-10-28 19:35:29 +00:00
self . _loader = None
2014-11-14 22:14:08 +00:00
self . _variable_manager = None
# every object gets a random uuid:
self . _uuid = uuid . uuid4 ( )
2014-10-08 19:59:24 +00:00
2015-03-23 20:19:13 +00:00
# and initialize the base attributes
self . _initialize_base_attributes ( )
2015-08-06 21:19:16 +00:00
try :
from __main__ import display
self . _display = display
except ImportError :
from ansible . utils . display import Display
self . _display = Display ( )
2015-03-24 06:15:30 +00:00
# The following three functions are used to programatically define data
# descriptors (aka properties) for the Attributes of all of the playbook
# objects (tasks, blocks, plays, etc).
#
# The function signature is a little strange because of how we define
# them. We use partial to give each method the name of the Attribute that
# it is for. Since partial prefills the positional arguments at the
# beginning of the function we end up with the first positional argument
# being allocated to the name instead of to the class instance (self) as
# normal. To deal with that we make the property name field the first
# positional argument and self the second arg.
#
# Because these methods are defined inside of the class, they get bound to
# the instance when the object is created. After we run partial on them
# and put the result back into the class as a property, they get bound
# a second time. This leads to self being placed in the arguments twice.
# To work around that, we mark the functions as @staticmethod so that the
# first binding to the instance doesn't happen.
2015-03-23 20:19:13 +00:00
@staticmethod
2015-03-24 06:15:30 +00:00
def _generic_g ( prop_name , self ) :
method = " _get_attr_ %s " % prop_name
2015-08-25 13:07:21 +00:00
if hasattr ( self , method ) :
2015-03-23 20:19:13 +00:00
return getattr ( self , method ) ( )
2015-09-26 15:11:55 +00:00
value = self . _attributes [ prop_name ]
if value is None and hasattr ( self , ' _get_parent_attribute ' ) :
value = self . _get_parent_attribute ( prop_name )
return value
2014-10-06 21:06:13 +00:00
2015-03-23 20:19:13 +00:00
@staticmethod
2015-03-24 06:15:30 +00:00
def _generic_s ( prop_name , self , value ) :
self . _attributes [ prop_name ] = value
2015-03-23 20:19:13 +00:00
@staticmethod
2015-03-24 06:15:30 +00:00
def _generic_d ( prop_name , self ) :
del self . _attributes [ prop_name ]
2014-10-19 05:14:30 +00:00
def _get_base_attributes ( self ) :
'''
Returns the list of attributes for this class ( or any subclass thereof ) .
If the attribute name starts with an underscore , it is removed
'''
base_attributes = dict ( )
for ( name , value ) in getmembers ( self . __class__ ) :
2014-10-06 21:06:13 +00:00
if isinstance ( value , Attribute ) :
2014-10-19 05:14:30 +00:00
if name . startswith ( ' _ ' ) :
name = name [ 1 : ]
base_attributes [ name ] = value
return base_attributes
2014-10-03 12:08:03 +00:00
2015-03-23 20:19:13 +00:00
def _initialize_base_attributes ( self ) :
# each class knows attributes set upon it, see Task.py for example
self . _attributes = dict ( )
for ( name , value ) in self . _get_base_attributes ( ) . items ( ) :
getter = partial ( self . _generic_g , name )
setter = partial ( self . _generic_s , name )
deleter = partial ( self . _generic_d , name )
2015-03-24 06:15:30 +00:00
# Place the property into the class so that cls.name is the
# property functions.
2015-03-23 20:19:13 +00:00
setattr ( Base , name , property ( getter , setter , deleter ) )
2015-03-24 06:15:30 +00:00
# Place the value into the instance so that the property can
# process and hold that value/
2015-03-23 20:19:13 +00:00
setattr ( self , name , value . default )
2015-03-24 01:42:28 +00:00
def preprocess_data ( self , ds ) :
2014-10-06 19:27:41 +00:00
''' infrequently used method to do some pre-processing of legacy terms '''
2015-03-23 23:14:34 +00:00
for base_class in self . __class__ . mro ( ) :
2015-03-24 01:42:28 +00:00
method = getattr ( self , " _preprocess_data_ %s " % base_class . __name__ . lower ( ) , None )
2015-03-20 19:13:51 +00:00
if method :
return method ( ds )
2014-10-06 19:27:41 +00:00
return ds
2014-11-14 22:14:08 +00:00
def load_data ( self , ds , variable_manager = None , loader = None ) :
2014-10-03 20:37:32 +00:00
''' walk the input datastructure and assign any values '''
2014-10-03 18:53:28 +00:00
2014-10-03 20:37:32 +00:00
assert ds is not None
2014-10-06 20:29:02 +00:00
2015-09-23 12:56:36 +00:00
# cache the datastructure internally
setattr ( self , ' _ds ' , ds )
2014-11-14 22:14:08 +00:00
# the variable manager class is used to manage and merge variables
# down to a single dictionary for reference in templating, etc.
self . _variable_manager = variable_manager
2014-10-28 19:35:29 +00:00
# the data loader class is used to parse data from strings and files
if loader is not None :
self . _loader = loader
else :
self . _loader = DataLoader ( )
2015-03-24 01:42:28 +00:00
# call the preprocess_data() function to massage the data into
# something we can more easily parse, and then call the validation
# function on it to ensure there are no incorrect key values
ds = self . preprocess_data ( ds )
2014-10-30 21:04:34 +00:00
self . _validate_attributes ( ds )
2014-10-03 20:37:32 +00:00
2015-08-05 17:54:00 +00:00
# Walk all attributes in the class. We sort them based on their priority
# so that certain fields can be loaded before others, if they are dependent.
2014-10-30 21:04:34 +00:00
# FIXME: we currently don't do anything with private attributes but
# may later decide to filter them out of 'ds' here.
2015-08-05 17:54:00 +00:00
base_attributes = self . _get_base_attributes ( )
for name , attr in sorted ( base_attributes . items ( ) , key = operator . itemgetter ( 1 ) ) :
2014-10-19 05:14:30 +00:00
# copy the value over unless a _load_field method is defined
if name in ds :
method = getattr ( self , ' _load_ %s ' % name , None )
if method :
self . _attributes [ name ] = method ( name , ds [ name ] )
else :
self . _attributes [ name ] = ds [ name ]
2014-10-08 19:59:24 +00:00
2014-11-14 22:14:08 +00:00
# run early, non-critical validation
2014-10-06 19:27:41 +00:00
self . validate ( )
2014-11-14 22:14:08 +00:00
# return the constructed object
2014-10-03 20:37:32 +00:00
return self
2014-11-14 22:14:08 +00:00
def get_ds ( self ) :
2015-03-23 23:38:51 +00:00
try :
2015-01-12 22:04:56 +00:00
return getattr ( self , ' _ds ' )
except AttributeError :
return None
2014-11-14 22:14:08 +00:00
2014-10-28 19:35:29 +00:00
def get_loader ( self ) :
return self . _loader
2014-10-03 20:37:32 +00:00
2014-11-14 22:14:08 +00:00
def get_variable_manager ( self ) :
return self . _variable_manager
2014-10-30 21:04:34 +00:00
def _validate_attributes ( self , ds ) :
'''
Ensures that there are no keys in the datastructure which do
not map to attributes for this object .
'''
2015-03-23 23:38:51 +00:00
valid_attrs = frozenset ( name for name in self . _get_base_attributes ( ) )
2014-10-30 21:04:34 +00:00
for key in ds :
if key not in valid_attrs :
2014-11-05 14:00:00 +00:00
raise AnsibleParserError ( " ' %s ' is not a valid attribute for a %s " % ( key , self . __class__ . __name__ ) , obj = ds )
2014-10-30 21:04:34 +00:00
2014-11-14 22:14:08 +00:00
def validate ( self , all_vars = dict ( ) ) :
2014-10-08 19:59:24 +00:00
''' validation that is done at parse time, not load time '''
2014-10-06 19:27:41 +00:00
# walk all fields in the object
2014-10-21 05:24:09 +00:00
for ( name , attribute ) in iteritems ( self . _get_base_attributes ( ) ) :
2014-10-08 19:59:24 +00:00
2014-10-19 05:14:30 +00:00
# run validator only if present
method = getattr ( self , ' _validate_ %s ' % name , None )
if method :
2014-11-14 22:14:08 +00:00
method ( attribute , name , getattr ( self , name ) )
2015-09-15 16:17:18 +00:00
else :
# and make sure the attribute is of the type it should be
value = getattr ( self , name )
if value is not None :
2015-09-15 17:08:10 +00:00
if attribute . isa == ' string ' and isinstance ( value , ( list , dict ) ) :
2015-09-15 16:17:18 +00:00
raise AnsibleParserError ( " The field ' %s ' is supposed to be a string type, however the incoming data structure is a %s " % ( name , type ( value ) ) , obj = self . get_ds ( ) )
2014-11-14 22:14:08 +00:00
def copy ( self ) :
'''
Create a copy of this object and return it .
'''
new_me = self . __class__ ( )
2014-10-03 20:37:32 +00:00
2015-03-23 23:38:51 +00:00
for name in self . _get_base_attributes ( ) :
2014-11-14 22:14:08 +00:00
setattr ( new_me , name , getattr ( self , name ) )
new_me . _loader = self . _loader
new_me . _variable_manager = self . _variable_manager
2015-06-27 19:18:18 +00:00
# if the ds value was set on the object, copy it to the new copy too
if hasattr ( self , ' _ds ' ) :
new_me . _ds = self . _ds
2014-11-14 22:14:08 +00:00
return new_me
2015-05-02 04:48:11 +00:00
def post_validate ( self , templar ) :
2014-10-06 19:27:41 +00:00
'''
we can ' t tell that everything is of the right type until we have
2014-10-08 19:59:24 +00:00
all the variables . Run basic types ( from isa ) as well as
2014-10-06 19:27:41 +00:00
any _post_validate_ < foo > functions .
2014-10-08 19:59:24 +00:00
'''
2014-10-06 19:27:41 +00:00
2014-11-14 22:14:08 +00:00
basedir = None
if self . _loader is not None :
basedir = self . _loader . get_basedir ( )
2015-07-15 17:53:59 +00:00
# save the omit value for later checking
omit_value = templar . _available_variables . get ( ' omit ' )
2014-11-14 22:14:08 +00:00
for ( name , attribute ) in iteritems ( self . _get_base_attributes ( ) ) :
if getattr ( self , name ) is None :
if not attribute . required :
continue
else :
raise AnsibleParserError ( " the field ' %s ' is required but was not set " % name )
2015-09-01 14:56:22 +00:00
elif not attribute . always_post_validate and self . __class__ . __name__ not in ( ' Task ' , ' Handler ' , ' PlayContext ' ) :
2015-08-28 19:54:20 +00:00
# Intermediate objects like Play() won't have their fields validated by
# default, as their values are often inherited by other objects and validated
# later, so we don't want them to fail out early
continue
2014-11-14 22:14:08 +00:00
try :
2015-06-22 15:23:23 +00:00
# Run the post-validator if present. These methods are responsible for
# using the given templar to template the values, if required.
2014-11-14 22:14:08 +00:00
method = getattr ( self , ' _post_validate_ %s ' % name , None )
if method :
2015-06-22 15:23:23 +00:00
value = method ( attribute , getattr ( self , name ) , templar )
2014-11-14 22:14:08 +00:00
else :
2015-06-22 15:23:23 +00:00
# if the attribute contains a variable, template it now
value = templar . template ( getattr ( self , name ) )
2015-07-15 17:53:59 +00:00
# if this evaluated to the omit value, set the value back to
# the default specified in the FieldAttribute and move on
if omit_value is not None and value == omit_value :
value = attribute . default
continue
2015-06-22 15:23:23 +00:00
# and make sure the attribute is of the type it should be
if value is not None :
2014-11-14 22:14:08 +00:00
if attribute . isa == ' string ' :
2015-09-04 05:39:08 +00:00
value = text_type ( value )
2014-11-14 22:14:08 +00:00
elif attribute . isa == ' int ' :
value = int ( value )
2015-08-26 16:02:21 +00:00
elif attribute . isa == ' float ' :
value = float ( value )
2014-11-14 22:14:08 +00:00
elif attribute . isa == ' bool ' :
value = boolean ( value )
2015-08-26 16:02:21 +00:00
elif attribute . isa == ' percent ' :
# special value, which may be an integer or float
# with an optional '%' at the end
if isinstance ( value , string_types ) and ' % ' in value :
value = value . replace ( ' % ' , ' ' )
value = float ( value )
2014-11-14 22:14:08 +00:00
elif attribute . isa == ' list ' :
2015-08-25 14:14:28 +00:00
if value is None :
value = [ ]
elif not isinstance ( value , list ) :
2014-11-14 22:14:08 +00:00
value = [ value ]
2015-06-27 05:01:08 +00:00
if attribute . listof is not None :
for item in value :
if not isinstance ( item , attribute . listof ) :
raise AnsibleParserError ( " the field ' %s ' should be a list of %s , but the item ' %s ' is a %s " % ( name , attribute . listof , item , type ( item ) ) , obj = self . get_ds ( ) )
2015-08-24 15:44:28 +00:00
elif attribute . required and attribute . listof == string_types :
if item is None or item . strip ( ) == " " :
raise AnsibleParserError ( " the field ' %s ' is required, and cannot have empty values " % ( name , ) , obj = self . get_ds ( ) )
2015-07-21 16:12:22 +00:00
elif attribute . isa == ' set ' :
2015-08-25 14:14:28 +00:00
if value is None :
value = set ( )
else :
if not isinstance ( value , ( list , set ) ) :
value = [ value ]
if not isinstance ( value , set ) :
value = set ( value )
elif attribute . isa == ' dict ' :
if value is None :
value = dict ( )
elif not isinstance ( value , dict ) :
raise TypeError ( " %s is not a dictionary " % value )
2014-11-14 22:14:08 +00:00
2015-01-08 16:51:54 +00:00
# and assign the massaged value back to the attribute field
setattr ( self , name , value )
2014-11-14 22:14:08 +00:00
2015-04-13 16:35:20 +00:00
except ( TypeError , ValueError ) as e :
2015-01-12 22:04:56 +00:00
raise AnsibleParserError ( " the field ' %s ' has an invalid value ( %s ), and could not be converted to an %s . Error was: %s " % ( name , value , attribute . isa , e ) , obj = self . get_ds ( ) )
2015-04-13 16:35:20 +00:00
except UndefinedError as e :
2015-06-18 18:27:20 +00:00
if templar . _fail_on_undefined_errors and name != ' name ' :
2015-01-12 22:04:56 +00:00
raise AnsibleParserError ( " the field ' %s ' has an invalid value, which appears to include a variable that is undefined. The error was: %s " % ( name , e ) , obj = self . get_ds ( ) )
2014-11-14 22:14:08 +00:00
def serialize ( self ) :
'''
Serializes the object derived from the base object into
a dictionary of values . This only serializes the field
attributes for the object , so this may need to be overridden
for any classes which wish to add additional items not stored
as field attributes .
'''
repr = dict ( )
2015-03-23 23:38:51 +00:00
for name in self . _get_base_attributes ( ) :
2014-11-14 22:14:08 +00:00
repr [ name ] = getattr ( self , name )
2015-02-12 18:11:08 +00:00
# serialize the uuid field
repr [ ' uuid ' ] = getattr ( self , ' _uuid ' )
2014-11-14 22:14:08 +00:00
return repr
def deserialize ( self , data ) :
'''
Given a dictionary of values , load up the field attributes for
this object . As with serialize ( ) , if there are any non - field
attribute data members , this method will need to be overridden
and extended .
'''
assert isinstance ( data , dict )
for ( name , attribute ) in iteritems ( self . _get_base_attributes ( ) ) :
if name in data :
setattr ( self , name , data [ name ] )
else :
setattr ( self , name , attribute . default )
2015-02-12 18:11:08 +00:00
# restore the UUID field
setattr ( self , ' _uuid ' , data . get ( ' uuid ' ) )
2014-10-03 19:25:21 +00:00
2015-08-07 04:05:42 +00:00
def _load_vars ( self , attr , ds ) :
'''
Vars in a play can be specified either as a dictionary directly , or
as a list of dictionaries . If the later , this method will turn the
list into a single dictionary .
'''
2015-08-28 15:35:43 +00:00
def _validate_variable_keys ( ds ) :
for key in ds :
if not isidentifier ( key ) :
raise TypeError ( " %s is not a valid variable name " % key )
2015-08-07 04:05:42 +00:00
try :
if isinstance ( ds , dict ) :
2015-08-28 15:35:43 +00:00
_validate_variable_keys ( ds )
2015-08-07 04:05:42 +00:00
return ds
elif isinstance ( ds , list ) :
all_vars = dict ( )
for item in ds :
if not isinstance ( item , dict ) :
raise ValueError
2015-08-28 15:35:43 +00:00
_validate_variable_keys ( item )
2015-08-07 04:05:42 +00:00
all_vars = combine_vars ( all_vars , item )
return all_vars
elif ds is None :
return { }
else :
raise ValueError
except ValueError :
raise AnsibleParserError ( " Vars in a %s must be specified as a dictionary, or a list of dictionaries " % self . __class__ . __name__ , obj = ds )
2015-08-28 17:30:27 +00:00
except TypeError as e :
2015-08-28 15:35:43 +00:00
raise AnsibleParserError ( " Invalid variable name in vars specified for %s : %s " % ( self . __class__ . __name__ , e ) , obj = ds )
2015-08-07 04:05:42 +00:00
2015-04-30 16:13:43 +00:00
def _extend_value ( self , value , new_value ) :
'''
Will extend the value given with new_value ( and will turn both
into lists if they are not so already ) . The values are run through
a set to remove duplicate values .
'''
if not isinstance ( value , list ) :
value = [ value ]
if not isinstance ( new_value , list ) :
new_value = [ new_value ]
2015-06-27 19:18:18 +00:00
#return list(set(value + new_value))
return [ i for i , _ in itertools . groupby ( value + new_value ) ]
2015-04-30 16:13:43 +00:00
2014-11-14 22:14:08 +00:00
def __getstate__ ( self ) :
return self . serialize ( )
def __setstate__ ( self , data ) :
self . __init__ ( )
self . deserialize ( data )