From 88482234e67b0966757413fc38130bdc4e3c888d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Fri, 13 Nov 2015 01:28:17 +0100 Subject: [PATCH] lxc connection plugin --- lib/ansible/plugins/connection/lxc.py | 215 ++++++++++++++++++ test/integration/test_connection.inventory | 18 ++ .../plugins/connections/test_connection.py | 4 + 3 files changed, 237 insertions(+) create mode 100644 lib/ansible/plugins/connection/lxc.py diff --git a/lib/ansible/plugins/connection/lxc.py b/lib/ansible/plugins/connection/lxc.py new file mode 100644 index 00000000000..36c87bb5933 --- /dev/null +++ b/lib/ansible/plugins/connection/lxc.py @@ -0,0 +1,215 @@ +# (c) 2015, Joerg Thalheim +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +import shutil +import traceback +import select +import fcntl +import errno +from ansible import errors +from ansible import constants as C +from ansible.plugins.connection import ConnectionBase +from ansible.utils.unicode import to_bytes + +HAS_LIBLXC = False +try: + import lxc as _lxc + HAS_LIBLXC = True +except ImportError: + pass + +class Connection(ConnectionBase): + ''' Local lxc based connections ''' + + transport = 'lxc' + has_pipelining = True + become_methods = frozenset(C.BECOME_METHODS) + + 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) + + def _communicate(self, 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, _ = 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) + + executable = to_bytes(self._play_context.executable, errors='strict') + local_cmd = [executable, '-c', to_bytes(cmd, errors='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='strict') + out_path = to_bytes(out_path, errors='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='strict') + out_path = to_bytes(out_path, errors='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 diff --git a/test/integration/test_connection.inventory b/test/integration/test_connection.inventory index 5fee43a2804..7b7348c2166 100644 --- a/test/integration/test_connection.inventory +++ b/test/integration/test_connection.inventory @@ -55,6 +55,24 @@ lxd-no-pipelining ansible_ssh_pipelining=false ansible_host=centos-7-amd64 ansible_connection=lxd +[lxc] +lxc-pipelining ansible_ssh_pipelining=true +lxc-no-pipelining ansible_ssh_pipelining=false +[lxc:vars] +# 1. install lxc +# 2. install python2-lxc +# $ pip install git+https://github.com/lxc/python2-lxc.git +# 3. create container: +# $ sudo lxc-create -t download -n centos-7-amd64 -- -d centos -r 7 -a amd64 +# 4. start container: +# $ sudo lxc-start -n centos-7-amd64 -d +# 5. run test: +# $ sudo -E make test_connection TEST_CONNECTION_FILTER=lxc +# 6. stop container +# $ sudo lxc-stop -n centos-7-amd64 +ansible_host=centos-7-amd64 +ansible_connection=lxc + [test_default:children] local chroot diff --git a/test/units/plugins/connections/test_connection.py b/test/units/plugins/connections/test_connection.py index 370768891d5..00d611c63c1 100644 --- a/test/units/plugins/connections/test_connection.py +++ b/test/units/plugins/connections/test_connection.py @@ -30,6 +30,7 @@ from ansible.plugins.connection import ConnectionBase #from ansible.plugins.connection.funcd import Connection as FuncdConnection #from ansible.plugins.connection.jail import Connection as JailConnection #from ansible.plugins.connection.libvirt_lxc import Connection as LibvirtLXCConnection +from ansible.plugins.connection.lxc import Connection as LxcConnection from ansible.plugins.connection.local import Connection as LocalConnection from ansible.plugins.connection.paramiko_ssh import Connection as ParamikoConnection from ansible.plugins.connection.ssh import Connection as SSHConnection @@ -89,6 +90,9 @@ class TestConnectionBaseClass(unittest.TestCase): # def test_libvirt_lxc_connection_module(self): # self.assertIsInstance(LibvirtLXCConnection(), LibvirtLXCConnection) + def test_lxc_connection_module(self): + self.assertIsInstance(LxcConnection(self.play_context, self.in_stream), LxcConnection) + def test_local_connection_module(self): self.assertIsInstance(LocalConnection(self.play_context, self.in_stream), LocalConnection)