summaryrefslogtreecommitdiffstats
path: root/ansible_collections/containers/podman/plugins/connection/podman.py
blob: 2ade9180dccffd56560c06d466e9e56d6b60e567 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# Based on the buildah connection plugin
# Copyright (c) 2018 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
#
# Connection plugin to interact with existing podman containers.
#   https://github.com/containers/libpod
#
# Written by: Tomas Tomecek (https://github.com/TomasTomecek)

from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

DOCUMENTATION = '''
    author: Tomas Tomecek (@TomasTomecek)
    name: podman
    short_description: Interact with an existing podman container
    description:
        - Run commands or put/fetch files to an existing container using podman tool.
    options:
      remote_addr:
        description:
          - The ID of the container you want to access.
        default: inventory_hostname
        vars:
          - name: ansible_host
          - name: inventory_hostname
          - name: ansible_podman_host
      remote_user:
        description:
            - User specified via name or UID which is used to execute commands inside the container. If you
              specify the user via UID, you must set C(ANSIBLE_REMOTE_TMP) to a path that exits
               inside the container and is writable by Ansible.
        ini:
          - section: defaults
            key: remote_user
        env:
          - name: ANSIBLE_REMOTE_USER
        vars:
          - name: ansible_user
      podman_extra_args:
        description:
          - Extra arguments to pass to the podman command line.
        default: ''
        ini:
          - section: defaults
            key: podman_extra_args
        vars:
          - name: ansible_podman_extra_args
        env:
          - name: ANSIBLE_PODMAN_EXTRA_ARGS
      podman_executable:
        description:
          - Executable for podman command.
        default: podman
        vars:
          - name: ansible_podman_executable
        env:
          - name: ANSIBLE_PODMAN_EXECUTABLE
'''

import os
import shlex
import shutil
import subprocess

from ansible.module_utils.common.process import get_bin_path
from ansible.errors import AnsibleError
from ansible.module_utils._text import to_bytes, to_native
from ansible.plugins.connection import ConnectionBase, ensure_connect
from ansible.utils.display import Display

display = Display()


# this _has to be_ named Connection
class Connection(ConnectionBase):
    """
    This is a connection plugin for podman. It uses podman binary to interact with the containers
    """

    # String used to identify this Connection class from other classes
    transport = 'containers.podman.podman'
    # We know that pipelining does not work with podman. Do not enable it, or
    # users will start containers and fail to connect to them.
    has_pipelining = False

    def __init__(self, play_context, new_stdin, *args, **kwargs):
        super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)

        self._container_id = self._play_context.remote_addr
        self._connected = False
        # container filesystem will be mounted here on host
        self._mount_point = None
        self.user = self._play_context.remote_user
        display.vvvv("Using podman connection from collection")

    def _podman(self, cmd, cmd_args=None, in_data=None, use_container_id=True):
        """
        run podman executable

        :param cmd: podman's command to execute (str or list)
        :param cmd_args: list of arguments to pass to the command (list of str/bytes)
        :param in_data: data passed to podman's stdin
        :param use_container_id: whether to append the container ID to the command
        :return: return code, stdout, stderr
        """
        podman_exec = self.get_option('podman_executable')
        try:
            podman_cmd = get_bin_path(podman_exec)
        except ValueError:
            raise AnsibleError("%s command not found in PATH" % podman_exec)
        if not podman_cmd:
            raise AnsibleError("%s command not found in PATH" % podman_exec)
        local_cmd = [podman_cmd]
        if self.get_option('podman_extra_args'):
            local_cmd += shlex.split(
                to_native(
                    self.get_option('podman_extra_args'),
                    errors='surrogate_or_strict'))
        if isinstance(cmd, str):
            local_cmd.append(cmd)
        else:
            local_cmd.extend(cmd)

        if use_container_id:
            local_cmd.append(self._container_id)
        if cmd_args:
            local_cmd += cmd_args
        local_cmd = [to_bytes(i, errors='surrogate_or_strict') for i in local_cmd]

        display.vvv("RUN %s" % (local_cmd,), host=self._container_id)
        p = subprocess.Popen(local_cmd, shell=False, stdin=subprocess.PIPE,
                             stdout=subprocess.PIPE, stderr=subprocess.PIPE)

        stdout, stderr = p.communicate(input=in_data)
        display.vvvvv("STDOUT %s" % stdout)
        display.vvvvv("STDERR %s" % stderr)
        display.vvvvv("RC CODE %s" % p.returncode)
        stdout = to_bytes(stdout, errors='surrogate_or_strict')
        stderr = to_bytes(stderr, errors='surrogate_or_strict')
        return p.returncode, stdout, stderr

    def _connect(self):
        """
        no persistent connection is being maintained, mount container's filesystem
        so we can easily access it
        """
        super(Connection, self)._connect()
        rc, self._mount_point, stderr = self._podman("mount")
        if rc != 0:
            display.vvvv("Failed to mount container %s: %s" % (self._container_id, stderr.strip()))
        elif not os.listdir(self._mount_point.strip()):
            display.vvvv("Failed to mount container with CGroups2: empty dir %s" % self._mount_point.strip())
            self._mount_point = None
        else:
            self._mount_point = self._mount_point.strip()
            display.vvvvv("MOUNTPOINT %s RC %s STDERR %r" % (self._mount_point, rc, stderr))
        self._connected = True

    @ensure_connect
    def exec_command(self, cmd, in_data=None, sudoable=False):
        """ run specified command in a running OCI container using podman """
        super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)

        # shlex.split has a bug with text strings on Python-2.6 and can only handle text strings on Python-3
        cmd_args_list = shlex.split(to_native(cmd, errors='surrogate_or_strict'))
        exec_args_list = ["exec"]
        if self.user:
            exec_args_list.extend(("--user", self.user))

        rc, stdout, stderr = self._podman(exec_args_list, cmd_args_list, in_data)

        display.vvvvv("STDOUT %r STDERR %r" % (stderr, stderr))
        return rc, stdout, stderr

    def put_file(self, in_path, out_path):
        """ Place a local file located in 'in_path' inside container at 'out_path' """
        super(Connection, self).put_file(in_path, out_path)
        display.vvv("PUT %s TO %s" % (in_path, out_path), host=self._container_id)
        if not self._mount_point or self.user:
            rc, stdout, stderr = self._podman(
                "cp", [in_path, self._container_id + ":" + out_path], use_container_id=False
            )
            if rc != 0:
                rc, stdout, stderr = self._podman(
                    "cp", ["--pause=false", in_path, self._container_id + ":" + out_path], use_container_id=False
                )
                if rc != 0:
                    raise AnsibleError(
                        "Failed to copy file from %s to %s in container %s\n%s" % (
                            in_path, out_path, self._container_id, stderr)
                    )
            if self.user:
                rc, stdout, stderr = self._podman(
                    "exec", ["chown", self.user, out_path])
            if rc != 0:
                raise AnsibleError(
                    "Failed to chown file %s for user %s in container %s\n%s" % (
                        out_path, self.user, self._container_id, stderr)
                )
        else:
            real_out_path = self._mount_point + to_bytes(out_path, errors='surrogate_or_strict')
            shutil.copyfile(
                to_bytes(in_path, errors='surrogate_or_strict'),
                to_bytes(real_out_path, errors='surrogate_or_strict')
            )

    def fetch_file(self, in_path, out_path):
        """ obtain file specified via 'in_path' from the container and place it at 'out_path' """
        super(Connection, self).fetch_file(in_path, out_path)
        display.vvv("FETCH %s TO %s" % (in_path, out_path), host=self._container_id)
        if not self._mount_point:
            rc, stdout, stderr = self._podman(
                "cp", [self._container_id + ":" + in_path, out_path], use_container_id=False)
            if rc != 0:
                raise AnsibleError("Failed to fetch file from %s to %s from container %s\n%s" % (
                    in_path, out_path, self._container_id, stderr))
        else:
            real_in_path = self._mount_point + to_bytes(in_path, errors='surrogate_or_strict')
            shutil.copyfile(
                to_bytes(real_in_path, errors='surrogate_or_strict'),
                to_bytes(out_path, errors='surrogate_or_strict')
            )

    def close(self):
        """ unmount container's filesystem """
        super(Connection, self).close()
        # we actually don't need to unmount since the container is mounted anyway
        # rc, stdout, stderr = self._podman("umount")
        # display.vvvvv("RC %s STDOUT %r STDERR %r" % (rc, stdout, stderr))
        self._connected = False