1# -*- coding: utf-8 -*- 2 3# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com> 4# 5# This file is part of Ansible 6# 7# Ansible is free software: you can redistribute it and/or modify 8# it under the terms of the GNU General Public License as published by 9# the Free Software Foundation, either version 3 of the License, or 10# (at your option) any later version. 11# 12# Ansible is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15# GNU General Public License for more details. 16# 17# You should have received a copy of the GNU General Public License 18# along with Ansible. If not, see <http://www.gnu.org/licenses/>. 19 20# Make coding more python3-ish 21from __future__ import (absolute_import, division, print_function) 22__metaclass__ = type 23 24import os 25import sys 26 27from ansible import constants as C 28from ansible import context 29from ansible.errors import AnsibleError 30from ansible.module_utils.compat.paramiko import paramiko 31from ansible.module_utils.six import iteritems 32from ansible.playbook.attribute import FieldAttribute 33from ansible.playbook.base import Base 34from ansible.plugins import get_plugin_class 35from ansible.utils.display import Display 36from ansible.plugins.loader import get_shell_plugin 37from ansible.utils.ssh_functions import check_for_controlpersist 38 39 40display = Display() 41 42 43__all__ = ['PlayContext'] 44 45 46TASK_ATTRIBUTE_OVERRIDES = ( 47 'become', 48 'become_user', 49 'become_pass', 50 'become_method', 51 'become_flags', 52 'connection', 53 'docker_extra_args', # TODO: remove 54 'delegate_to', 55 'no_log', 56 'remote_user', 57) 58 59RESET_VARS = ( 60 'ansible_connection', 61 'ansible_user', 62 'ansible_host', 63 'ansible_port', 64 65 # TODO: ??? 66 'ansible_docker_extra_args', 67 'ansible_ssh_host', 68 'ansible_ssh_pass', 69 'ansible_ssh_port', 70 'ansible_ssh_user', 71 'ansible_ssh_private_key_file', 72 'ansible_ssh_pipelining', 73 'ansible_ssh_executable', 74) 75 76 77class PlayContext(Base): 78 79 ''' 80 This class is used to consolidate the connection information for 81 hosts in a play and child tasks, where the task may override some 82 connection/authentication information. 83 ''' 84 85 # base 86 _module_compression = FieldAttribute(isa='string', default=C.DEFAULT_MODULE_COMPRESSION) 87 _shell = FieldAttribute(isa='string') 88 _executable = FieldAttribute(isa='string', default=C.DEFAULT_EXECUTABLE) 89 90 # connection fields, some are inherited from Base: 91 # (connection, port, remote_user, environment, no_log) 92 _remote_addr = FieldAttribute(isa='string') 93 _password = FieldAttribute(isa='string') 94 _timeout = FieldAttribute(isa='int', default=C.DEFAULT_TIMEOUT) 95 _connection_user = FieldAttribute(isa='string') 96 _private_key_file = FieldAttribute(isa='string', default=C.DEFAULT_PRIVATE_KEY_FILE) 97 _pipelining = FieldAttribute(isa='bool', default=C.ANSIBLE_PIPELINING) 98 99 # networking modules 100 _network_os = FieldAttribute(isa='string') 101 102 # docker FIXME: remove these 103 _docker_extra_args = FieldAttribute(isa='string') 104 105 # ssh # FIXME: remove these 106 _ssh_executable = FieldAttribute(isa='string', default=C.ANSIBLE_SSH_EXECUTABLE) 107 _ssh_args = FieldAttribute(isa='string', default=C.ANSIBLE_SSH_ARGS) 108 _ssh_common_args = FieldAttribute(isa='string') 109 _sftp_extra_args = FieldAttribute(isa='string') 110 _scp_extra_args = FieldAttribute(isa='string') 111 _ssh_extra_args = FieldAttribute(isa='string') 112 _ssh_transfer_method = FieldAttribute(isa='string', default=C.DEFAULT_SSH_TRANSFER_METHOD) 113 114 # ??? 115 _connection_lockfd = FieldAttribute(isa='int') 116 117 # privilege escalation fields 118 _become = FieldAttribute(isa='bool') 119 _become_method = FieldAttribute(isa='string') 120 _become_user = FieldAttribute(isa='string') 121 _become_pass = FieldAttribute(isa='string') 122 _become_exe = FieldAttribute(isa='string', default=C.DEFAULT_BECOME_EXE) 123 _become_flags = FieldAttribute(isa='string', default=C.DEFAULT_BECOME_FLAGS) 124 _prompt = FieldAttribute(isa='string') 125 126 # general flags 127 _verbosity = FieldAttribute(isa='int', default=0) 128 _only_tags = FieldAttribute(isa='set', default=set) 129 _skip_tags = FieldAttribute(isa='set', default=set) 130 131 _start_at_task = FieldAttribute(isa='string') 132 _step = FieldAttribute(isa='bool', default=False) 133 134 # "PlayContext.force_handlers should not be used, the calling code should be using play itself instead" 135 _force_handlers = FieldAttribute(isa='bool', default=False) 136 137 def __init__(self, play=None, passwords=None, connection_lockfd=None): 138 # Note: play is really not optional. The only time it could be omitted is when we create 139 # a PlayContext just so we can invoke its deserialize method to load it from a serialized 140 # data source. 141 142 super(PlayContext, self).__init__() 143 144 if passwords is None: 145 passwords = {} 146 147 self.password = passwords.get('conn_pass', '') 148 self.become_pass = passwords.get('become_pass', '') 149 150 self._become_plugin = None 151 152 self.prompt = '' 153 self.success_key = '' 154 155 # a file descriptor to be used during locking operations 156 self.connection_lockfd = connection_lockfd 157 158 # set options before play to allow play to override them 159 if context.CLIARGS: 160 self.set_attributes_from_cli() 161 162 if play: 163 self.set_attributes_from_play(play) 164 165 def set_attributes_from_plugin(self, plugin): 166 # generic derived from connection plugin, temporary for backwards compat, in the end we should not set play_context properties 167 168 # get options for plugins 169 options = C.config.get_configuration_definitions(get_plugin_class(plugin), plugin._load_name) 170 for option in options: 171 if option: 172 flag = options[option].get('name') 173 if flag: 174 setattr(self, flag, self.connection.get_option(flag)) 175 176 def set_attributes_from_play(self, play): 177 self.force_handlers = play.force_handlers 178 179 def set_attributes_from_cli(self): 180 ''' 181 Configures this connection information instance with data from 182 options specified by the user on the command line. These have a 183 lower precedence than those set on the play or host. 184 ''' 185 if context.CLIARGS.get('timeout', False): 186 self.timeout = int(context.CLIARGS['timeout']) 187 188 # From the command line. These should probably be used directly by plugins instead 189 # For now, they are likely to be moved to FieldAttribute defaults 190 self.private_key_file = context.CLIARGS.get('private_key_file') # Else default 191 self.verbosity = context.CLIARGS.get('verbosity') # Else default 192 self.ssh_common_args = context.CLIARGS.get('ssh_common_args') # Else default 193 self.ssh_extra_args = context.CLIARGS.get('ssh_extra_args') # Else default 194 self.sftp_extra_args = context.CLIARGS.get('sftp_extra_args') # Else default 195 self.scp_extra_args = context.CLIARGS.get('scp_extra_args') # Else default 196 197 # Not every cli that uses PlayContext has these command line args so have a default 198 self.start_at_task = context.CLIARGS.get('start_at_task', None) # Else default 199 200 def set_task_and_variable_override(self, task, variables, templar): 201 ''' 202 Sets attributes from the task if they are set, which will override 203 those from the play. 204 205 :arg task: the task object with the parameters that were set on it 206 :arg variables: variables from inventory 207 :arg templar: templar instance if templating variables is needed 208 ''' 209 210 new_info = self.copy() 211 212 # loop through a subset of attributes on the task object and set 213 # connection fields based on their values 214 for attr in TASK_ATTRIBUTE_OVERRIDES: 215 if hasattr(task, attr): 216 attr_val = getattr(task, attr) 217 if attr_val is not None: 218 setattr(new_info, attr, attr_val) 219 220 # next, use the MAGIC_VARIABLE_MAPPING dictionary to update this 221 # connection info object with 'magic' variables from the variable list. 222 # If the value 'ansible_delegated_vars' is in the variables, it means 223 # we have a delegated-to host, so we check there first before looking 224 # at the variables in general 225 if task.delegate_to is not None: 226 # In the case of a loop, the delegated_to host may have been 227 # templated based on the loop variable, so we try and locate 228 # the host name in the delegated variable dictionary here 229 delegated_host_name = templar.template(task.delegate_to) 230 delegated_vars = variables.get('ansible_delegated_vars', dict()).get(delegated_host_name, dict()) 231 232 delegated_transport = C.DEFAULT_TRANSPORT 233 for transport_var in C.MAGIC_VARIABLE_MAPPING.get('connection'): 234 if transport_var in delegated_vars: 235 delegated_transport = delegated_vars[transport_var] 236 break 237 238 # make sure this delegated_to host has something set for its remote 239 # address, otherwise we default to connecting to it by name. This 240 # may happen when users put an IP entry into their inventory, or if 241 # they rely on DNS for a non-inventory hostname 242 for address_var in ('ansible_%s_host' % delegated_transport,) + C.MAGIC_VARIABLE_MAPPING.get('remote_addr'): 243 if address_var in delegated_vars: 244 break 245 else: 246 display.debug("no remote address found for delegated host %s\nusing its name, so success depends on DNS resolution" % delegated_host_name) 247 delegated_vars['ansible_host'] = delegated_host_name 248 249 # reset the port back to the default if none was specified, to prevent 250 # the delegated host from inheriting the original host's setting 251 for port_var in ('ansible_%s_port' % delegated_transport,) + C.MAGIC_VARIABLE_MAPPING.get('port'): 252 if port_var in delegated_vars: 253 break 254 else: 255 if delegated_transport == 'winrm': 256 delegated_vars['ansible_port'] = 5986 257 else: 258 delegated_vars['ansible_port'] = C.DEFAULT_REMOTE_PORT 259 260 # and likewise for the remote user 261 for user_var in ('ansible_%s_user' % delegated_transport,) + C.MAGIC_VARIABLE_MAPPING.get('remote_user'): 262 if user_var in delegated_vars and delegated_vars[user_var]: 263 break 264 else: 265 delegated_vars['ansible_user'] = task.remote_user or self.remote_user 266 else: 267 delegated_vars = dict() 268 269 # setup shell 270 for exe_var in C.MAGIC_VARIABLE_MAPPING.get('executable'): 271 if exe_var in variables: 272 setattr(new_info, 'executable', variables.get(exe_var)) 273 274 attrs_considered = [] 275 for (attr, variable_names) in iteritems(C.MAGIC_VARIABLE_MAPPING): 276 for variable_name in variable_names: 277 if attr in attrs_considered: 278 continue 279 # if delegation task ONLY use delegated host vars, avoid delegated FOR host vars 280 if task.delegate_to is not None: 281 if isinstance(delegated_vars, dict) and variable_name in delegated_vars: 282 setattr(new_info, attr, delegated_vars[variable_name]) 283 attrs_considered.append(attr) 284 elif variable_name in variables: 285 setattr(new_info, attr, variables[variable_name]) 286 attrs_considered.append(attr) 287 # no else, as no other vars should be considered 288 289 # become legacy updates -- from inventory file (inventory overrides 290 # commandline) 291 for become_pass_name in C.MAGIC_VARIABLE_MAPPING.get('become_pass'): 292 if become_pass_name in variables: 293 break 294 295 # make sure we get port defaults if needed 296 if new_info.port is None and C.DEFAULT_REMOTE_PORT is not None: 297 new_info.port = int(C.DEFAULT_REMOTE_PORT) 298 299 # special overrides for the connection setting 300 if len(delegated_vars) > 0: 301 # in the event that we were using local before make sure to reset the 302 # connection type to the default transport for the delegated-to host, 303 # if not otherwise specified 304 for connection_type in C.MAGIC_VARIABLE_MAPPING.get('connection'): 305 if connection_type in delegated_vars: 306 break 307 else: 308 remote_addr_local = new_info.remote_addr in C.LOCALHOST 309 inv_hostname_local = delegated_vars.get('inventory_hostname') in C.LOCALHOST 310 if remote_addr_local and inv_hostname_local: 311 setattr(new_info, 'connection', 'local') 312 elif getattr(new_info, 'connection', None) == 'local' and (not remote_addr_local or not inv_hostname_local): 313 setattr(new_info, 'connection', C.DEFAULT_TRANSPORT) 314 315 # we store original in 'connection_user' for use of network/other modules that fallback to it as login user 316 # connection_user to be deprecated once connection=local is removed for, as local resets remote_user 317 if new_info.connection == 'local': 318 if not new_info.connection_user: 319 new_info.connection_user = new_info.remote_user 320 321 # set no_log to default if it was not previously set 322 if new_info.no_log is None: 323 new_info.no_log = C.DEFAULT_NO_LOG 324 325 if task.check_mode is not None: 326 new_info.check_mode = task.check_mode 327 328 if task.diff is not None: 329 new_info.diff = task.diff 330 331 return new_info 332 333 def set_become_plugin(self, plugin): 334 self._become_plugin = plugin 335 336 def make_become_cmd(self, cmd, executable=None): 337 """ helper function to create privilege escalation commands """ 338 display.deprecated( 339 "PlayContext.make_become_cmd should not be used, the calling code should be using become plugins instead", 340 version="2.12" 341 ) 342 343 if not cmd or not self.become: 344 return cmd 345 346 become_method = self.become_method 347 348 # load/call become plugins here 349 plugin = self._become_plugin 350 351 if plugin: 352 options = { 353 'become_exe': self.become_exe or become_method, 354 'become_flags': self.become_flags or '', 355 'become_user': self.become_user, 356 'become_pass': self.become_pass 357 } 358 plugin.set_options(direct=options) 359 360 if not executable: 361 executable = self.executable 362 363 shell = get_shell_plugin(executable=executable) 364 cmd = plugin.build_become_command(cmd, shell) 365 # for backwards compat: 366 if self.become_pass: 367 self.prompt = plugin.prompt 368 else: 369 raise AnsibleError("Privilege escalation method not found: %s" % become_method) 370 371 return cmd 372 373 def update_vars(self, variables): 374 ''' 375 Adds 'magic' variables relating to connections to the variable dictionary provided. 376 In case users need to access from the play, this is a legacy from runner. 377 ''' 378 379 for prop, var_list in C.MAGIC_VARIABLE_MAPPING.items(): 380 try: 381 if 'become' in prop: 382 continue 383 384 var_val = getattr(self, prop) 385 for var_opt in var_list: 386 if var_opt not in variables and var_val is not None: 387 variables[var_opt] = var_val 388 except AttributeError: 389 continue 390 391 def _get_attr_connection(self): 392 ''' connections are special, this takes care of responding correctly ''' 393 conn_type = None 394 if self._attributes['connection'] == 'smart': 395 conn_type = 'ssh' 396 # see if SSH can support ControlPersist if not use paramiko 397 if not check_for_controlpersist(self.ssh_executable) and paramiko is not None: 398 conn_type = "paramiko" 399 400 # if someone did `connection: persistent`, default it to using a persistent paramiko connection to avoid problems 401 elif self._attributes['connection'] == 'persistent' and paramiko is not None: 402 conn_type = 'paramiko' 403 404 if conn_type: 405 self.connection = conn_type 406 407 return self._attributes['connection'] 408