From 2de773477f62d5f7a2b786a8577baadfc159181b Mon Sep 17 00:00:00 2001 From: objectified Date: Tue, 11 Aug 2015 15:19:41 -0400 Subject: [PATCH 1/6] allow ansible to connect to docker containers --- lib/ansible/plugins/connections/docker.py | 119 ++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 lib/ansible/plugins/connections/docker.py diff --git a/lib/ansible/plugins/connections/docker.py b/lib/ansible/plugins/connections/docker.py new file mode 100644 index 00000000000..48b0ba8fce4 --- /dev/null +++ b/lib/ansible/plugins/connections/docker.py @@ -0,0 +1,119 @@ +# Based on the chroot connection plugin by Maykel Moya +# +# Connection plugin for configuring docker containers +# (c) 2014, Lorin Hochstein +# (c) 2015, Leendert Brouwer +# +# 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 . + +import os +import subprocess +import time + +from ansible import errors +from ansible.plugins.connections import ConnectionBase + +BUFSIZE = 65536 + +class Connection(ConnectionBase): + + def __init__(self, play_context, new_stdin, *args, **kwargs): + super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs) + + if 'docker_command' in kwargs: + self.docker_cmd = kwargs['docker_command'] + else: + self.docker_cmd = 'docker' + + @property + def transport(self): + return 'docker' + + def _connect(self, port=None): + """ Connect to the container. Nothing to do """ + return self + + def exec_command(self, cmd, tmp_path, sudo_user=None, sudoable=False, + executable='/bin/sh', in_data=None, su=None, + su_user=None): + + """ Run a command on the local host """ + + # Don't currently support su + if su or su_user: + raise errors.AnsibleError("Internal Error: this module does not " + "support running commands via su") + + if in_data: + raise errors.AnsibleError("Internal Error: this module does not " + "support optimized module pipelining") + + if sudoable and sudo_user: + raise errors.AnsibleError("Internal Error: this module does not " + "support running commands via sudo") + + if executable: + local_cmd = [self.docker_cmd, "exec", self._play_context.remote_addr, executable, + '-c', cmd] + else: + local_cmd = '%s exec "%s" %s' % (self.docker_cmd, self._play_context.remote_addr, cmd) + + self._display.vvv("EXEC %s" % (local_cmd), host=self._play_context.remote_addr) + p = subprocess.Popen(local_cmd, + shell=isinstance(local_cmd, basestring), + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + stdout, stderr = p.communicate() + return (p.returncode, '', stdout, stderr) + + # Docker doesn't have native support for copying files into running + # containers, so we use docker exec to implement this + def put_file(self, in_path, out_path): + """ Transfer a file from local to container """ + args = [self.docker_cmd, "exec", "-i", self._play_context.remote_addr, "bash", "-c", + "dd of=%s bs=%s" % (format(out_path), BUFSIZE)] + + self._display.vvv("PUT %s TO %s" % (in_path, out_path), host=self._play_context.remote_addr) + + if not os.path.exists(in_path): + raise errors.AnsibleFileNotFound( + "file or module does not exist: %s" % in_path) + p = subprocess.Popen(args, stdin=open(in_path), + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + p.communicate() + + def fetch_file(self, in_path, out_path): + """ Fetch a file from container to local. """ + # out_path is the final file path, but docker takes a directory, not a + # file path + out_dir = os.path.dirname(out_path) + + args = [self.docker_cmd, "cp", "%s:%s" % (self._play_context.remote_addr, in_path), out_dir] + + self._display.vvv("FETCH %s TO %s" % (in_path, out_path), host=self._play_context.remote_addr) + p = subprocess.Popen(args, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + p.communicate() + + # Rename if needed + actual_out_path = os.path.join(out_dir, os.path.basename(in_path)) + if actual_out_path != out_path: + os.rename(actual_out_path, out_path) + + def close(self): + """ Terminate the connection. Nothing to do for Docker""" + pass From 8f2a6a9faeecb05d142e6476bcccb94e2eb212c9 Mon Sep 17 00:00:00 2001 From: objectified Date: Mon, 24 Aug 2015 02:28:54 -0400 Subject: [PATCH 2/6] use docker cp when docker >=1.8.0 --- lib/ansible/plugins/connections/docker.py | 62 ++++++++++++++++++++--- 1 file changed, 54 insertions(+), 8 deletions(-) diff --git a/lib/ansible/plugins/connections/docker.py b/lib/ansible/plugins/connections/docker.py index 48b0ba8fce4..b52b16f433a 100644 --- a/lib/ansible/plugins/connections/docker.py +++ b/lib/ansible/plugins/connections/docker.py @@ -22,6 +22,7 @@ import os import subprocess import time +import re from ansible import errors from ansible.plugins.connections import ConnectionBase @@ -38,10 +39,45 @@ class Connection(ConnectionBase): else: self.docker_cmd = 'docker' + self.can_copy_bothways = False + + docker_version = self._get_docker_version() + if self.compare_versions(docker_version, '1.8.0') >= 0: + self.can_copy_bothways = True + + def _get_docker_version(self): + + def sanitize_version(version): + return re.sub('[^0-9a-zA-Z\.]', '', version) + + cmd = [self.docker_cmd, 'version'] + + cmd_output = subprocess.check_output(cmd) + + for line in cmd_output.split('\n'): + if line.startswith('Server version:'): # old docker versions + return sanitize_version(line.split()[2]) + + # no result yet, must be newer Docker version + new_docker_cmd = [ + self.docker_cmd, + 'version', '--format', "'{{.Server.Version}}'" + ] + + cmd_output = subprocess.check_output(new_docker_cmd) + + return sanitize_version(cmd_output) + @property def transport(self): return 'docker' + def compare_versions(self, version1, version2): + # Source: https://stackoverflow.com/questions/1714027/version-number-comparison + def normalize(v): + return [int(x) for x in re.sub(r'(\.0+)*$','', v).split(".")] + return cmp(normalize(version1), normalize(version2)) + def _connect(self, port=None): """ Connect to the container. Nothing to do """ return self @@ -84,17 +120,27 @@ class Connection(ConnectionBase): # containers, so we use docker exec to implement this def put_file(self, in_path, out_path): """ Transfer a file from local to container """ - args = [self.docker_cmd, "exec", "-i", self._play_context.remote_addr, "bash", "-c", - "dd of=%s bs=%s" % (format(out_path), BUFSIZE)] - - self._display.vvv("PUT %s TO %s" % (in_path, out_path), host=self._play_context.remote_addr) - if not os.path.exists(in_path): raise errors.AnsibleFileNotFound( "file or module does not exist: %s" % in_path) - p = subprocess.Popen(args, stdin=open(in_path), - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - p.communicate() + + self._display.vvv("PUT %s TO %s" % (in_path, out_path), host=self._play_context.remote_addr) + + if self.can_copy_bothways: # only docker >= 1.8.1 can do this natively + args = [ + self.docker_cmd, + "cp", + "%s" % in_path, + "%s:%s" % (self._play_context.remote_addr, out_path) + ] + subprocess.check_call(args) + else: + args = [self.docker_cmd, "exec", "-i", self._play_context.remote_addr, "bash", "-c", + "dd of=%s bs=%s" % (format(out_path), BUFSIZE)] + + p = subprocess.Popen(args, stdin=open(in_path), + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + p.communicate() def fetch_file(self, in_path, out_path): """ Fetch a file from container to local. """ From 3a5522a22cfe74e74cb967b172cb8e9eefb45ff0 Mon Sep 17 00:00:00 2001 From: objectified Date: Mon, 24 Aug 2015 12:12:28 -0400 Subject: [PATCH 3/6] fake being connected for logging purposes --- lib/ansible/plugins/connections/docker.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/ansible/plugins/connections/docker.py b/lib/ansible/plugins/connections/docker.py index b52b16f433a..f66b466e319 100644 --- a/lib/ansible/plugins/connections/docker.py +++ b/lib/ansible/plugins/connections/docker.py @@ -80,6 +80,12 @@ class Connection(ConnectionBase): def _connect(self, port=None): """ Connect to the container. Nothing to do """ + if not self._connected: + self._display.vvv("ESTABLISH LOCAL CONNECTION FOR USER: {0}".format( + self._play_context.remote_user, host=self._play_context.remote_addr) + ) + self._connected = True + return self def exec_command(self, cmd, tmp_path, sudo_user=None, sudoable=False, @@ -162,4 +168,4 @@ class Connection(ConnectionBase): def close(self): """ Terminate the connection. Nothing to do for Docker""" - pass + self._connected = False From d9723069c5d94af2b110251cbeeb96125d18a9d1 Mon Sep 17 00:00:00 2001 From: objectified Date: Mon, 24 Aug 2015 12:31:02 -0400 Subject: [PATCH 4/6] align exec_command() definition with local.py --- lib/ansible/plugins/connections/docker.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/lib/ansible/plugins/connections/docker.py b/lib/ansible/plugins/connections/docker.py index f66b466e319..d03ace4a23c 100644 --- a/lib/ansible/plugins/connections/docker.py +++ b/lib/ansible/plugins/connections/docker.py @@ -24,6 +24,8 @@ import subprocess import time import re +import ansible.constants as C + from ansible import errors from ansible.plugins.connections import ConnectionBase @@ -88,25 +90,16 @@ class Connection(ConnectionBase): return self - def exec_command(self, cmd, tmp_path, sudo_user=None, sudoable=False, - executable='/bin/sh', in_data=None, su=None, - su_user=None): - + def exec_command(self, cmd, tmp_path, in_data=None, sudoable=False): """ Run a command on the local host """ + super(Connection, self).exec_command(cmd, tmp_path, in_data=in_data, sudoable=sudoable) # Don't currently support su - if su or su_user: - raise errors.AnsibleError("Internal Error: this module does not " - "support running commands via su") - if in_data: raise errors.AnsibleError("Internal Error: this module does not " "support optimized module pipelining") - if sudoable and sudo_user: - raise errors.AnsibleError("Internal Error: this module does not " - "support running commands via sudo") - + executable = C.DEFAULT_EXECUTABLE.split()[0] if C.DEFAULT_EXECUTABLE else None if executable: local_cmd = [self.docker_cmd, "exec", self._play_context.remote_addr, executable, '-c', cmd] From c39fb43ad908dc9c4ab36bd6b64af188f65836bd Mon Sep 17 00:00:00 2001 From: objectified Date: Tue, 25 Aug 2015 02:06:01 -0400 Subject: [PATCH 5/6] added Maintainer comment header --- lib/ansible/plugins/connections/docker.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/ansible/plugins/connections/docker.py b/lib/ansible/plugins/connections/docker.py index d03ace4a23c..983769622bc 100644 --- a/lib/ansible/plugins/connections/docker.py +++ b/lib/ansible/plugins/connections/docker.py @@ -4,6 +4,8 @@ # (c) 2014, Lorin Hochstein # (c) 2015, Leendert Brouwer # +# Maintainer: Leendert Brouwer (https://github.com/objectified) +# # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify From b1785a03619c953ec9a1af7fd41fd6a647423ff2 Mon Sep 17 00:00:00 2001 From: objectified Date: Tue, 25 Aug 2015 02:18:37 -0400 Subject: [PATCH 6/6] replace compare_versions() with distutils.version --- lib/ansible/plugins/connections/docker.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/lib/ansible/plugins/connections/docker.py b/lib/ansible/plugins/connections/docker.py index 983769622bc..0168f96fbef 100644 --- a/lib/ansible/plugins/connections/docker.py +++ b/lib/ansible/plugins/connections/docker.py @@ -26,6 +26,8 @@ import subprocess import time import re +from distutils.version import LooseVersion + import ansible.constants as C from ansible import errors @@ -46,7 +48,7 @@ class Connection(ConnectionBase): self.can_copy_bothways = False docker_version = self._get_docker_version() - if self.compare_versions(docker_version, '1.8.0') >= 0: + if LooseVersion(docker_version) >= LooseVersion('1.8.0'): self.can_copy_bothways = True def _get_docker_version(self): @@ -76,12 +78,6 @@ class Connection(ConnectionBase): def transport(self): return 'docker' - def compare_versions(self, version1, version2): - # Source: https://stackoverflow.com/questions/1714027/version-number-comparison - def normalize(v): - return [int(x) for x in re.sub(r'(\.0+)*$','', v).split(".")] - return cmp(normalize(version1), normalize(version2)) - def _connect(self, port=None): """ Connect to the container. Nothing to do """ if not self._connected: