diff --git a/ansible_mitogen/connection.py b/ansible_mitogen/connection.py index 643ecb94..0884da54 100644 --- a/ansible_mitogen/connection.py +++ b/ansible_mitogen/connection.py @@ -58,15 +58,6 @@ import ansible_mitogen.transport_config LOG = logging.getLogger(__name__) -def optional_secret(value): - """ - Wrap `value` in :class:`mitogen.core.Secret` if it is not :data:`None`, - otherwise return :data:`None`. - """ - if value is not None: - return mitogen.core.Secret(value) - - def optional_int(value): """ Convert `value` to an integer if it is not :data:`None`, otherwise return @@ -111,7 +102,7 @@ def _connect_ssh(spec): 'check_host_keys': check_host_keys, 'hostname': spec.remote_addr(), 'username': spec.remote_user(), - 'password': optional_secret(spec.password()), + 'password': spec.password(), 'port': spec.port(), 'python_path': spec.python_path(), 'identity_file': private_key_file, @@ -235,7 +226,7 @@ def _connect_su(spec): 'enable_lru': True, 'kwargs': { 'username': spec.become_user(), - 'password': optional_secret(spec.become_pass()), + 'password': spec.become_pass(), 'python_path': spec.python_path(), 'su_path': spec.become_exe(), 'connect_timeout': spec.timeout(), @@ -252,7 +243,7 @@ def _connect_sudo(spec): 'enable_lru': True, 'kwargs': { 'username': spec.become_user(), - 'password': optional_secret(spec.become_pass()), + 'password': spec.become_pass(), 'python_path': spec.python_path(), 'sudo_path': spec.become_exe(), 'connect_timeout': spec.timeout(), @@ -270,7 +261,7 @@ def _connect_doas(spec): 'enable_lru': True, 'kwargs': { 'username': spec.become_user(), - 'password': optional_secret(spec.become_pass()), + 'password': spec.become_pass(), 'python_path': spec.python_path(), 'doas_path': spec.become_exe(), 'connect_timeout': spec.timeout(), @@ -286,7 +277,7 @@ def _connect_mitogen_su(spec): 'method': 'su', 'kwargs': { 'username': spec.remote_user(), - 'password': optional_secret(spec.password()), + 'password': spec.password(), 'python_path': spec.python_path(), 'su_path': spec.become_exe(), 'connect_timeout': spec.timeout(), @@ -302,7 +293,7 @@ def _connect_mitogen_sudo(spec): 'method': 'sudo', 'kwargs': { 'username': spec.remote_user(), - 'password': optional_secret(spec.password()), + 'password': spec.password(), 'python_path': spec.python_path(), 'sudo_path': spec.become_exe(), 'connect_timeout': spec.timeout(), @@ -319,7 +310,7 @@ def _connect_mitogen_doas(spec): 'method': 'doas', 'kwargs': { 'username': spec.remote_user(), - 'password': optional_secret(spec.password()), + 'password': spec.password(), 'python_path': spec.python_path(), 'doas_path': spec.become_exe(), 'connect_timeout': spec.timeout(), diff --git a/ansible_mitogen/transport_config.py b/ansible_mitogen/transport_config.py index 2ad37c33..d46eb565 100644 --- a/ansible_mitogen/transport_config.py +++ b/ansible_mitogen/transport_config.py @@ -1,9 +1,39 @@ +# Copyright 2017, David Wilson +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from __future__ import absolute_import +from __future__ import unicode_literals """ -Mitogen extends Ansible's regular host configuration mechanism in several ways -that require quite a lot of care: +Mitogen extends Ansible's target configuration mechanism in several ways that +require some care: -* Some per-task configurables in Ansible like ansible_python_interpreter are +* Per-task configurables in Ansible like ansible_python_interpreter are connection-layer configurables in Mitogen. They must be extracted during each task execution to form the complete connection-layer configuration. @@ -13,28 +43,31 @@ that require quite a lot of care: * Mitogen allows connections to be delegated to other machines. Ansible has no internal framework for this, and so Mitogen must figure out a delegated - connection configuration all for itself. This means it cannot reuse much of - the Ansible machinery for building a connection configuration, as that - machinery is deeply spread and out hard-wired to expect Ansible's usual mode - of operation. - -For delegated connections, Ansible's PlayContext information is reused where -possible, but for proxy hops, configurations are built up using the HostVars -magic class to call VariableManager.get_vars() behind the scenes on our behalf. -Where Ansible has multiple sources of a configuration item, for example, -ansible_ssh_extra_args, Mitogen must (ideally perfectly) reproduce how Ansible -arrives at its value, without using mechanisms that are hard-wired or change -across Ansible versions. + connection configuration all on its own. It cannot reuse much of the Ansible + machinery for building a connection configuration, as that machinery is + deeply spread out and hard-wired to expect Ansible's usual mode of operation. + +For normal and delegate_to connections, Ansible's PlayContext is reused where +possible to maximize compatibility, but for proxy hops, configurations are +built up using the HostVars magic class to call VariableManager.get_vars() +behind the scenes on our behalf. Where Ansible has multiple sources of a +configuration item, for example, ansible_ssh_extra_args, Mitogen must (ideally +perfectly) reproduce how Ansible arrives at its value, without using mechanisms +that are hard-wired or change across Ansible versions. That is what this file is for. It exports two spec classes, one that takes all information from PlayContext, and another that takes (almost) all information from HostVars. """ +import abc import os import ansible.utils.shlex import ansible.constants as C +from ansible.module_utils.six import with_metaclass + + import mitogen.core @@ -47,11 +80,201 @@ def parse_python_path(s): return ansible.utils.shlex.shlex_split(s) -class PlayContextSpec: +def optional_secret(value): + """ + Wrap `value` in :class:`mitogen.core.Secret` if it is not :data:`None`, + otherwise return :data:`None`. + """ + if value is not None: + return mitogen.core.Secret(value) + + +class Spec(with_metaclass(abc.ABCMeta, object)): + """ + A source for variables that comprise a connection configuration. + """ + + @abc.abstractmethod + def transport(self): + """ + The name of the Ansible plug-in implementing the connection. + """ + + @abc.abstractmethod + def inventory_name(self): + """ + The name of the target being connected to as it appears in Ansible's + inventory. + """ + + @abc.abstractmethod + def remote_addr(self): + """ + The network address of the target, or for container and other special + targets, some other unique identifier. + """ + + @abc.abstractmethod + def remote_user(self): + """ + The username of the login account on the target. + """ + + @abc.abstractmethod + def password(self): + """ + The password of the login account on the target. + """ + + @abc.abstractmethod + def become(self): + """ + :data:`True` if privilege escalation should be active. + """ + + @abc.abstractmethod + def become_method(self): + """ + The name of the Ansible become method to use. + """ + + @abc.abstractmethod + def become_user(self): + """ + The username of the target account for become. + """ + + @abc.abstractmethod + def become_pass(self): + """ + The password of the target account for become. + """ + + @abc.abstractmethod + def port(self): + """ + The port of the login service on the target machine. + """ + + @abc.abstractmethod + def python_path(self): + """ + Path to the Python interpreter on the target machine. + """ + + @abc.abstractmethod + def private_key_file(self): + """ + Path to the SSH private key file to use to login. + """ + + @abc.abstractmethod + def ssh_executable(self): + """ + Path to the SSH executable. + """ + + @abc.abstractmethod + def timeout(self): + """ + The generic timeout for all connections. + """ + + @abc.abstractmethod + def ansible_ssh_timeout(self): + """ + The SSH-specific timeout for a connection. + """ + + @abc.abstractmethod + def ssh_args(self): + """ + The list of additional arguments that should be included in an SSH + invocation. + """ + + @abc.abstractmethod + def become_exe(self): + """ + The path to the executable implementing the become method on the remote + machine. + """ + + @abc.abstractmethod + def sudo_args(self): + """ + The list of additional arguments that should be included in a become + invocation. + """ + # TODO: split out into sudo_args/become_args. + + @abc.abstractmethod + def mitogen_via(self): + """ + The value of the mitogen_via= variable for this connection. Indicates + the connection should be established via an intermediary. + """ + + @abc.abstractmethod + def mitogen_kind(self): + """ + The type of container to use with the "setns" transport. + """ + + @abc.abstractmethod + def mitogen_docker_path(self): + """ + The path to the "docker" program for the 'docker' transport. + """ + + @abc.abstractmethod + def mitogen_kubectl_path(self): + """ + The path to the "kubectl" program for the 'docker' transport. + """ + + @abc.abstractmethod + def mitogen_lxc_path(self): + """ + The path to the "lxc" program for the 'lxd' transport. + """ + + @abc.abstractmethod + def mitogen_lxc_attach_path(self): + """ + The path to the "lxc-attach" program for the 'lxc' transport. + """ + + @abc.abstractmethod + def mitogen_lxc_info_path(self): + """ + The path to the "lxc-info" program for the 'lxc' transport. + """ + + @abc.abstractmethod + def mitogen_machinectl_path(self): + """ + The path to the "machinectl" program for the 'setns' transport. + """ + + @abc.abstractmethod + def mitogen_ssh_debug_level(self): + """ + The SSH debug level. + """ + + @abc.abstractmethod + def extra_args(self): + """ + Connection-specific arguments. + """ + + +class PlayContextSpec(Spec): """ - Return a dict representing all important connection configuration, allowing - the same functions to work regardless of whether configuration came from - play_context (direct connection) or host vars (mitogen_via=). + PlayContextSpec takes almost all its information as-is from Ansible's + PlayContext. It is used for normal connections and delegate_to connections, + and should always be accurate. """ def __init__(self, connection, play_context, transport, inventory_name): self._connection = connection @@ -81,10 +304,10 @@ class PlayContextSpec: return self._play_context.become_user def become_pass(self): - return self._play_context.become_pass + return optional_secret(self._play_context.become_pass) def password(self): - return self._play_context.password + return optional_secret(self._play_context.password) def port(self): return self._play_context.port @@ -107,7 +330,7 @@ class PlayContextSpec: return ( self._connection.get_task_var('ansible_timeout') or self._connection.get_task_var('ansible_ssh_timeout') or - self._play_context.timeout + self.timeout() ) def ssh_args(self): @@ -165,7 +388,23 @@ class PlayContextSpec: return self._connection.get_extra_args() -class MitogenViaSpec: +class MitogenViaSpec(Spec): + """ + MitogenViaSpec takes most of its information from the HostVars of the + running task. HostVars is a lightweight wrapper around VariableManager, so + it is better to say that VariableManager.get_vars() is the ultimate source + of MitogenViaSpec's information. + + Due to this, mitogen_via= hosts must have all their configuration + information represented as host and group variables. We cannot use any + per-task configuration, as all that data belongs to the real target host. + + Ansible uses all kinds of strange historical logic for calculating + variables, including making their precedence configurable. MitogenViaSpec + must ultimately reimplement all of that logic. It is likely that if you are + having a configruation problem with connection delegation, the answer to + your problem lies in the method implementations below! + """ def __init__(self, inventory_name, host_vars, become_method, become_user): self._inventory_name = inventory_name @@ -205,14 +444,14 @@ class MitogenViaSpec: return self._become_user def become_pass(self): - return ( + return optional_secret( # TODO: Might have to come from PlayContext. self._host_vars.get('ansible_become_password') or self._host_vars.get('ansible_become_pass') ) def password(self): - return ( + return optional_secret( # TODO: Might have to come from PlayContext. self._host_vars.get('ansible_ssh_pass') or self._host_vars.get('ansible_password') @@ -225,13 +464,11 @@ class MitogenViaSpec: ) def python_path(self): - s = parse_python_path( + return parse_python_path( self._host_vars.get('ansible_python_interpreter') # This variable has no default for remote hosts. For local hosts it # is sys.executable. ) - print('hi ho', self.inventory_name(), s) - return s def private_key_file(self): # TODO: must come from PlayContext too.