# This plugin was adapted from two sources: # - https://github.com/antifuchs/ansible-sshlxd-connection # - https://github.com/austinhyde/ansible-sshjail # # The MIT License (MIT) # # Copyright (c) 2022 Hugo Peixoto (hugopeixoto.net) # Copyright (c) 2016 Andreas Fuchs # Copyright (c) 2015-2018, Austin Hyde (@austinhyde) # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from __future__ import (absolute_import, division, print_function) import os import os.path import pipes from ansible.errors import AnsibleError from ansible.plugins.connection.ssh import Connection as SSHConnection from contextlib import contextmanager __metaclass__ = type DOCUMENTATION = ''' connection: ssh short_description: connect to LXD containers using lxc comand via ssh client binary description: - Use the ssh client library to access the LXD host and then use lxc commands to interact with the container. - Options are the same as ssh, except got host variable wich should be @ author: Andreas Fuchs (@antifuchs), Joao Silva(@wnke) version_added: historical options: host: description: Hostname/ip to connect to. default: inventory_hostname vars: - name: ansible_host - name: ansible_ssh_host host_key_checking: description: Determines if ssh should check host keys type: boolean ini: - section: defaults key: 'host_key_checking' - section: ssh_connection key: 'host_key_checking' version_added: '2.5' env: - name: ANSIBLE_HOST_KEY_CHECKING - name: ANSIBLE_SSH_HOST_KEY_CHECKING version_added: '2.5' vars: - name: ansible_host_key_checking version_added: '2.5' - name: ansible_ssh_host_key_checking version_added: '2.5' password: description: Authentication password for the C(remote_user). Can be supplied as CLI option. vars: - name: ansible_password - name: ansible_ssh_pass sshpass_prompt: description: Password prompt that sshpass should search for. Supported by sshpass 1.06 and up default: '' ini: - section: 'ssh_connection' key: 'sshpass_prompt' env: - name: ANSIBLE_SSHPASS_PROMPT vars: - name: ansible_sshpass_prompt version_added: '2.10' ssh_args: description: Arguments to pass to all ssh cli tools default: '-C -o ControlMaster=auto -o ControlPersist=60s' ini: - section: 'ssh_connection' key: 'ssh_args' env: - name: ANSIBLE_SSH_ARGS vars: - name: ansible_ssh_args version_added: '2.7' ssh_common_args: description: Common extra args for all ssh CLI tools ini: - section: 'ssh_connection' key: 'ssh_common_args' version_added: '2.7' env: - name: ANSIBLE_SSH_COMMON_ARGS version_added: '2.7' vars: - name: ansible_ssh_common_args ssh_executable: default: ssh description: - This defines the location of the ssh binary. It defaults to ``ssh`` which will use the first ssh binary available in $PATH. - This option is usually not required, it might be useful when access to system ssh is restricted, or when using ssh wrappers to connect to remote hosts. env: [{name: ANSIBLE_SSH_EXECUTABLE}] ini: - {key: ssh_executable, section: ssh_connection} #const: ANSIBLE_SSH_EXECUTABLE version_added: "2.2" vars: - name: ansible_ssh_executable version_added: '2.7' sftp_executable: default: sftp description: - This defines the location of the sftp binary. It defaults to `sftp` which will use the first binary available in $PATH. env: [{name: ANSIBLE_SFTP_EXECUTABLE}] ini: - {key: sftp_executable, section: ssh_connection} version_added: "2.6" scp_executable: default: scp description: - This defines the location of the scp binary. It defaults to `scp` which will use the first binary available in $PATH. env: [{name: ANSIBLE_SCP_EXECUTABLE}] ini: - {key: scp_executable, section: ssh_connection} version_added: "2.6" scp_extra_args: description: Extra exclusive to the 'scp' CLI vars: - name: ansible_scp_extra_args sftp_extra_args: description: Extra exclusive to the 'sftp' CLI vars: - name: ansible_sftp_extra_args ssh_extra_args: description: Extra exclusive to the 'ssh' CLI vars: - name: ansible_ssh_extra_args env: - name: ANSIBLE_SSH_EXTRA_ARGS version_added: '2.7' ini: - key: ssh_extra_args section: ssh_connection version_added: '2.7' reconnection_retries: description: Number of attempts to connect. default: 0 type: integer env: - name: ANSIBLE_SSH_RETRIES ini: - section: connection key: retries - section: ssh_connection key: retries vars: - name: ansible_ssh_retries version_added: '2.7' port: description: Remote port to connect to. type: int default: 22 ini: - section: defaults key: remote_port env: - name: ANSIBLE_REMOTE_PORT vars: - name: ansible_port - name: ansible_ssh_port remote_user: description: - User name with which to login to the remote server, normally set by the remote_user keyword. - If no user is supplied, Ansible will let the ssh client binary choose the user as it normally ini: - section: defaults key: remote_user env: - name: ANSIBLE_REMOTE_USER vars: - name: ansible_user - name: ansible_ssh_user pipelining: env: - name: ANSIBLE_PIPELINING - name: ANSIBLE_SSH_PIPELINING ini: - section: connection key: pipelining - section: ssh_connection key: pipelining vars: - name: ansible_pipelining - name: ansible_ssh_pipelining private_key_file: description: - Path to private key file to use for authentication ini: - section: defaults key: private_key_file env: - name: ANSIBLE_PRIVATE_KEY_FILE vars: - name: ansible_private_key_file - name: ansible_ssh_private_key_file control_path: description: - This is the location to save ssh's ControlPath sockets, it uses ssh's variable substitution. - Since 2.3, if null, ansible will generate a unique hash. Use `%(directory)s` to indicate where to use the control dir path setting. env: - name: ANSIBLE_SSH_CONTROL_PATH ini: - key: control_path section: ssh_connection control_path_dir: default: ~/.ansible/cp description: - This sets the directory to use for ssh control path if the control path setting is null. - Also, provides the `%(directory)s` variable for the control path setting. env: - name: ANSIBLE_SSH_CONTROL_PATH_DIR ini: - section: ssh_connection key: control_path_dir sftp_batch_mode: default: 'yes' description: 'TODO: write it' env: [{name: ANSIBLE_SFTP_BATCH_MODE}] ini: - {key: sftp_batch_mode, section: ssh_connection} type: bool ssh_transfer_method: default: smart description: - "Preferred method to use when transferring files over ssh" - Setting to 'smart' (default) will try them in order, until one succeeds or they all fail - Using 'piped' creates an ssh pipe with ``dd`` on either side to copy the data choices: ['sftp', 'scp', 'piped', 'smart'] env: [{name: ANSIBLE_SSH_TRANSFER_METHOD}] ini: - {key: transfer_method, section: ssh_connection} vars: - name: ansible_ssh_transfer_method version_added: '2.12' scp_if_ssh: default: smart description: - "Prefered method to use when transfering files over ssh" - When set to smart, Ansible will try them until one succeeds or they all fail - If set to True, it will force 'scp', if False it will use 'sftp' env: [{name: ANSIBLE_SCP_IF_SSH}] ini: - {key: scp_if_ssh, section: ssh_connection} use_tty: version_added: '2.5' default: 'yes' description: add -tt to ssh commands to force tty allocation env: [{name: ANSIBLE_SSH_USETTY}] ini: - {key: usetty, section: ssh_connection} type: bool yaml: {key: connection.usetty} timeout: default: 10 description: - This is the default ammount of time we will wait while establishing an ssh connection - It also controls how long we can wait to access reading the connection once established (select on the socket) env: - name: ANSIBLE_TIMEOUT - name: ANSIBLE_SSH_TIMEOUT version_added: '2.11' ini: - key: timeout section: defaults - key: timeout section: ssh_connection version_added: '2.11' vars: - name: ansible_ssh_timeout version_added: '2.11' cli: - name: timeout type: integer pkcs11_provider: version_added: '2.12' default: "" description: - "PKCS11 SmartCard provider such as opensc, example: /usr/local/lib/opensc-pkcs11.so" - Requires sshpass version 1.06+, sshpass must support the -P option. env: [{name: ANSIBLE_PKCS11_PROVIDER}] ini: - {key: pkcs11_provider, section: ssh_connection} vars: - name: ansible_ssh_pkcs11_provider ''' try: from __main__ import display except ImportError: from ansible.utils.display import Display display = Display() class Connection(SSHConnection): ''' ssh based connections ''' transport = 'sshlxd' def __init__(self, *args, **kwargs): super(Connection, self).__init__(*args, **kwargs) self.inventory_hostname = self.host self.containerspec, self.host = self.host.split('@', 1) self.connector = None def get_container_id(self): return self.containerspec def get_container_connector(self): return 'lxc' def _strip_sudo(self, executable, cmd): # Get the command without sudo sudoless = cmd.rsplit(executable + ' -c ', 1)[1] # Get the quotes quotes = sudoless.partition('echo')[0] # Get the string between the quotes cmd = sudoless[len(quotes):-len(quotes + '?')] return cmd def host_command(self, cmd, do_become=False): return super(Connection, self).exec_command(cmd, in_data=None, sudoable=True) def exec_command(self, cmd, in_data=None, executable='/bin/sh', sudoable=True): ''' run a command in the container ''' self.set_option('host', self.host) cmd = '%s exec %s -- %s' % (self.get_container_connector(), self.get_container_id(), cmd) return super(Connection, self).exec_command(cmd, in_data, True) def container_path(self, path): return self.get_container_id() + path @contextmanager def tempfile(self): code, stdout, stderr = self.host_command('mktemp') if code != 0: raise AnsibleError("failed to make temp file:\n%s\n%s" % (stdout, stderr)) tmp = stdout.strip().decode('utf-8').split('\n')[-1] yield tmp code, stdout, stderr = self.host_command(' '.join(['rm', tmp])) if code != 0: raise AnsibleError("failed to remove temp file %s:\n%s\n%s" % (tmp, stdout, stderr)) def put_file(self, in_path, out_path): ''' transfer a file from local to remote container ''' with self.tempfile() as tmp: super(Connection, self).put_file(in_path, tmp) self.host_command(' '.join(['/snap/bin/lxc', 'exec', self.get_container_id(), '--', 'mkdir', '-p', os.path.dirname(out_path)]), do_become=True) self.host_command(' '.join(['/snap/bin/lxc', 'file', 'push', '--debug', tmp, self.container_path(out_path)]), do_become=True) def fetch_file(self, in_path, out_path): ''' fetch a file from remote to local ''' with self.tempfile() as tmp: self.host_command(' '.join(['/snap/bin/lxc', 'file', 'pull', self.container_path(in_path), tmp]), do_become=True) super(Connection, self).fetch_file(tmp, out_path) def close(self): ''' Close the connection, nothing to do for us ''' super(Connection, self).close()