(本文基于Ansible 2.7)
前两天有位朋友问到是否可以使用用户名+口令的登录方式来代替预先分发RSA公钥到目标主机以实现免密登录的方式,这无疑是可以的,而且很简单。只需要在Inventory中给host增加两个variable,它们分别是:
- ansible_ssh_user
- ansible_ssh_pass
并安装sshpass包,以便使用非交互的方式提供登录口令。
在lib/ansible/constants.py中定义了一个字典——MAGIC_VARIABLE_MAPPING :
MAGIC_VARIABLE_MAPPING = dict(
# base
connection=('ansible_connection', ),
module_compression=('ansible_module_compression', ),
shell=('ansible_shell_type', ),
executable=('ansible_shell_executable', ),
# connection common
remote_addr=('ansible_ssh_host', 'ansible_host'),
remote_user=('ansible_ssh_user', 'ansible_user'),
password=('ansible_ssh_pass', 'ansible_password'),
port=('ansible_ssh_port', 'ansible_port'),
pipelining=('ansible_ssh_pipelining', 'ansible_pipelining'),
timeout=('ansible_ssh_timeout', 'ansible_timeout'),
private_key_file=('ansible_ssh_private_key_file', 'ansible_private_key_file'),
# networking modules
network_os=('ansible_network_os', ),
connection_user=('ansible_connection_user',),
# ssh TODO: remove
ssh_executable=('ansible_ssh_executable', ),
ssh_common_args=('ansible_ssh_common_args', ),
sftp_extra_args=('ansible_sftp_extra_args', ),
scp_extra_args=('ansible_scp_extra_args', ),
ssh_extra_args=('ansible_ssh_extra_args', ),
ssh_transfer_method=('ansible_ssh_transfer_method', ),
# docker TODO: remove
docker_extra_args=('ansible_docker_extra_args', ),
# become
become=('ansible_become', ),
become_method=('ansible_become_method', ),
become_user=('ansible_become_user', ),
become_pass=('ansible_become_password', 'ansible_become_pass'),
become_exe=('ansible_become_exe', ),
become_flags=('ansible_become_flags', ),
# deprecated
sudo=('ansible_sudo', ),
sudo_user=('ansible_sudo_user', ),
sudo_pass=('ansible_sudo_password', 'ansible_sudo_pass'),
sudo_exe=('ansible_sudo_exe', ),
sudo_flags=('ansible_sudo_flags', ),
su=('ansible_su', ),
su_user=('ansible_su_user', ),
su_pass=('ansible_su_password', 'ansible_su_pass'),
su_exe=('ansible_su_exe', ),
su_flags=('ansible_su_flags', ),
)
这个字典使用来做变量名映射的(这句是废话,看命名就看出来了)
简单总结一下,就是如果发现有变量存在于该字典的values中,则将该变量的值赋给变量名为values对应的key的变量。如:变量中如果存在ansible_ssh_port或ansible_port,那么它的值会被赋给port。
下面是ansible源码的分析过程:
MAGIC_VARIABLE_MAPPING 这个字典主要在lib/ansible/playbook/play_context.py中的set_task_and_variable_override方法有多处使用,其中涉及本文的,在366-379行:
attrs_considered = []
for (attr, variable_names) in iteritems(C.MAGIC_VARIABLE_MAPPING):
for variable_name in variable_names:
if attr in attrs_considered:
continue
# if delegation task ONLY use delegated host vars, avoid delegated FOR host vars
if task.delegate_to is not None:
if isinstance(delegated_vars, dict) and variable_name in delegated_vars:
setattr(new_info, attr, delegated_vars[variable_name])
attrs_considered.append(attr)
elif variable_name in variables:
setattr(new_info, attr, variables[variable_name])
attrs_considered.append(attr)
# no else, as no other vars should be considered
set_task_and_variable_override这个方法在StrategyBase._execute_meta和TaskExecutor._execute中调用,variables是传入的参数。其中,TaskExecutor._execute由TaskExecutor.run调用,而TaskExecutor.run又是WorkerProcess.run中实际执行task并返回结果的核心内容(详见lib/ansible/executor/process/worker.py),在WorkerProcess中的变量是包含了host variable的。正是在WorkerProcess.run去具体执行一个任务的时候,把inventory中的host变量ansible_ssh_user和ansible_ssh_pass的值覆盖到了PlayContext的remote_user,password。
remote_user和password这两个变量的使用,在两个connection plugin中:
lib/ansible/plugins/ssh.py中的_build_command方法,549-559行,做了sshpass环境的相关检查,就是看有没有安装这个包:
if self._play_context.password:
if not self._sshpass_available():
raise AnsibleError("to use the 'ssh' connection type with passwords, you must install the sshpass program")
614-620行,拼接了登录的用户名:
user = self._play_context.remote_user
if user:
self._add_args(
b_command,
(b"-o", b"User=" + to_bytes(self._play_context.remote_user, errors='surrogate_or_strict')),
u"ANSIBLE_REMOTE_USER/remote_user/ansible_user/user/-u set"
)
_bare_run方法的793-804行,写入口令:
# If we are using SSH password authentication, write the password into
# the pipe we opened in _build_command.
if self._play_context.password:
os.close(self.sshpass_pipe[0])
try:
os.write(self.sshpass_pipe[1], to_bytes(self._play_context.password) + b'\n')
except OSError as e:
# Ignore broken pipe errors if the sshpass process has exited.
if e.errno != errno.EPIPE or p.poll() is None:
raise
os.close(self.sshpass_pipe[1])
lib/ansible/plugins/paramiko_ssh.py的_connect_uncached方法,353-363行,用用户名和口令执行ssh连接:
ssh.connect(
self._play_context.remote_addr.lower(),
username=self._play_context.remote_user,
allow_agent=allow_agent,
look_for_keys=self.get_option('look_for_keys'),
key_filename=key_filename,
password=self._play_context.password,
timeout=self._play_context.timeout,
port=port,
**ssh_connect_kwargs
)
以及479-486行的_connect_sftp方法(许多传输文件的操作,例如fetch模块,如果使用paramiko_ssh连接目标,就是使用sftp来实现的):
def _connect_sftp(self):
cache_key = "%s__%s__" % (self._play_context.remote_addr, self._play_context.remote_user)
if cache_key in SFTP_CONNECTION_CACHE:
return SFTP_CONNECTION_CACHE[cache_key]
else:
result = SFTP_CONNECTION_CACHE[cache_key] = self._connect().ssh.open_sftp()
return result
OK,至此在动态清单中使用Host Variable存储ssh登录凭证并使用改凭证连接目标主机的过程就说完了。其实最有参考意义的是MAGIC_VARIABLE_MAPPING 这个字典。除ansible_ssh_user和ansible_ssh_pass之外,还有其他的诸多变量,可以放置在host variable中,并起到相应的作用。