diff options
Diffstat (limited to 'ansible_collections/community/general/plugins/connection/lxc.py')
-rw-r--r-- | ansible_collections/community/general/plugins/connection/lxc.py | 230 |
1 files changed, 230 insertions, 0 deletions
diff --git a/ansible_collections/community/general/plugins/connection/lxc.py b/ansible_collections/community/general/plugins/connection/lxc.py new file mode 100644 index 000000000..adf3eec1c --- /dev/null +++ b/ansible_collections/community/general/plugins/connection/lxc.py @@ -0,0 +1,230 @@ +# -*- coding: utf-8 -*- +# (c) 2015, Joerg Thalheim <joerg@higgsboson.tk> +# Copyright (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = ''' + author: Joerg Thalheim (!UNKNOWN) <joerg@higgsboson.tk> + name: lxc + short_description: Run tasks in lxc containers via lxc python library + description: + - Run commands or put/fetch files to an existing lxc container using lxc python library + options: + remote_addr: + description: + - Container identifier + default: inventory_hostname + vars: + - name: ansible_host + - name: ansible_lxc_host + executable: + default: /bin/sh + description: + - Shell executable + vars: + - name: ansible_executable + - name: ansible_lxc_executable +''' + +import os +import shutil +import traceback +import select +import fcntl +import errno + +HAS_LIBLXC = False +try: + import lxc as _lxc + HAS_LIBLXC = True +except ImportError: + pass + +from ansible import errors +from ansible.module_utils.common.text.converters import to_bytes, to_native +from ansible.plugins.connection import ConnectionBase + + +class Connection(ConnectionBase): + """ Local lxc based connections """ + + transport = 'community.general.lxc' + has_pipelining = True + default_user = 'root' + + def __init__(self, play_context, new_stdin, *args, **kwargs): + super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs) + + self.container_name = self._play_context.remote_addr + self.container = None + + def _connect(self): + """ connect to the lxc; nothing to do here """ + super(Connection, self)._connect() + + if not HAS_LIBLXC: + msg = "lxc bindings for python2 are not installed" + raise errors.AnsibleError(msg) + + if self.container: + return + + self._display.vvv("THIS IS A LOCAL LXC DIR", host=self.container_name) + self.container = _lxc.Container(self.container_name) + if self.container.state == "STOPPED": + raise errors.AnsibleError("%s is not running" % self.container_name) + + @staticmethod + def _communicate(pid, in_data, stdin, stdout, stderr): + buf = {stdout: [], stderr: []} + read_fds = [stdout, stderr] + if in_data: + write_fds = [stdin] + else: + write_fds = [] + while len(read_fds) > 0 or len(write_fds) > 0: + try: + ready_reads, ready_writes, dummy = select.select(read_fds, write_fds, []) + except select.error as e: + if e.args[0] == errno.EINTR: + continue + raise + for fd in ready_writes: + in_data = in_data[os.write(fd, in_data):] + if len(in_data) == 0: + write_fds.remove(fd) + for fd in ready_reads: + data = os.read(fd, 32768) + if not data: + read_fds.remove(fd) + buf[fd].append(data) + + (pid, returncode) = os.waitpid(pid, 0) + + return returncode, b"".join(buf[stdout]), b"".join(buf[stderr]) + + def _set_nonblocking(self, fd): + flags = fcntl.fcntl(fd, fcntl.F_GETFL) | os.O_NONBLOCK + fcntl.fcntl(fd, fcntl.F_SETFL, flags) + return fd + + def exec_command(self, cmd, in_data=None, sudoable=False): + """ run a command on the chroot """ + super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable) + + # python2-lxc needs bytes. python3-lxc needs text. + executable = to_native(self._play_context.executable, errors='surrogate_or_strict') + local_cmd = [executable, '-c', to_native(cmd, errors='surrogate_or_strict')] + + read_stdout, write_stdout = None, None + read_stderr, write_stderr = None, None + read_stdin, write_stdin = None, None + + try: + read_stdout, write_stdout = os.pipe() + read_stderr, write_stderr = os.pipe() + + kwargs = { + 'stdout': self._set_nonblocking(write_stdout), + 'stderr': self._set_nonblocking(write_stderr), + 'env_policy': _lxc.LXC_ATTACH_CLEAR_ENV + } + + if in_data: + read_stdin, write_stdin = os.pipe() + kwargs['stdin'] = self._set_nonblocking(read_stdin) + + self._display.vvv("EXEC %s" % (local_cmd), host=self.container_name) + pid = self.container.attach(_lxc.attach_run_command, local_cmd, **kwargs) + if pid == -1: + msg = "failed to attach to container %s" % self.container_name + raise errors.AnsibleError(msg) + + write_stdout = os.close(write_stdout) + write_stderr = os.close(write_stderr) + if read_stdin: + read_stdin = os.close(read_stdin) + + return self._communicate(pid, + in_data, + write_stdin, + read_stdout, + read_stderr) + finally: + fds = [read_stdout, + write_stdout, + read_stderr, + write_stderr, + read_stdin, + write_stdin] + for fd in fds: + if fd: + os.close(fd) + + def put_file(self, in_path, out_path): + ''' transfer a file from local to lxc ''' + super(Connection, self).put_file(in_path, out_path) + self._display.vvv("PUT %s TO %s" % (in_path, out_path), host=self.container_name) + in_path = to_bytes(in_path, errors='surrogate_or_strict') + out_path = to_bytes(out_path, errors='surrogate_or_strict') + + if not os.path.exists(in_path): + msg = "file or module does not exist: %s" % in_path + raise errors.AnsibleFileNotFound(msg) + try: + src_file = open(in_path, "rb") + except IOError: + traceback.print_exc() + raise errors.AnsibleError("failed to open input file to %s" % in_path) + try: + def write_file(args): + with open(out_path, 'wb+') as dst_file: + shutil.copyfileobj(src_file, dst_file) + try: + self.container.attach_wait(write_file, None) + except IOError: + traceback.print_exc() + msg = "failed to transfer file to %s" % out_path + raise errors.AnsibleError(msg) + finally: + src_file.close() + + def fetch_file(self, in_path, out_path): + ''' fetch a file from lxc to local ''' + super(Connection, self).fetch_file(in_path, out_path) + self._display.vvv("FETCH %s TO %s" % (in_path, out_path), host=self.container_name) + in_path = to_bytes(in_path, errors='surrogate_or_strict') + out_path = to_bytes(out_path, errors='surrogate_or_strict') + + try: + dst_file = open(out_path, "wb") + except IOError: + traceback.print_exc() + msg = "failed to open output file %s" % out_path + raise errors.AnsibleError(msg) + try: + def write_file(args): + try: + with open(in_path, 'rb') as src_file: + shutil.copyfileobj(src_file, dst_file) + finally: + # this is needed in the lxc child process + # to flush internal python buffers + dst_file.close() + try: + self.container.attach_wait(write_file, None) + except IOError: + traceback.print_exc() + msg = "failed to transfer file from %s to %s" % (in_path, out_path) + raise errors.AnsibleError(msg) + finally: + dst_file.close() + + def close(self): + ''' terminate the connection; nothing to do here ''' + super(Connection, self).close() + self._connected = False |