summaryrefslogtreecommitdiff
path: root/contrib/python/podman/client.py
blob: ad166eb06acb1e75e2b9a2ad7d59988f40c23b32 (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
"""A client for communicating with a Podman varlink service."""
import errno
import os
from urllib.parse import urlparse

from varlink import Client as VarlinkClient
from varlink import VarlinkError

from .libs import cached_property
from .libs.containers import Containers
from .libs.errors import error_factory
from .libs.images import Images
from .libs.system import System
from .libs.tunnel import Context, Portal, Tunnel


class BaseClient(object):
    """Context manager for API workers to access varlink."""

    def __call__(self):
        """Support being called for old API."""
        return self

    @classmethod
    def factory(cls,
                uri=None,
                interface='io.projectatomic.podman',
                *args,
                **kwargs):
        """Construct a Client based on input."""
        if uri is None:
            raise ValueError('uri is required and cannot be None')
        if interface is None:
            raise ValueError('interface is required and cannot be None')

        unsupported = set(kwargs.keys()).difference(
            ('uri', 'interface', 'remote_uri', 'identity_file'))
        if unsupported:
            raise ValueError('Unknown keyword arguments: {}'.format(
                ', '.join(unsupported)))

        local_path = urlparse(uri).path
        if local_path == '':
            raise ValueError('path is required for uri,'
                             ' expected format "unix://path_to_socket"')

        if kwargs.get('remote_uri') or kwargs.get('identity_file'):
            # Remote access requires the full tuple of information
            if kwargs.get('remote_uri') is None:
                raise ValueError(
                    'remote is required,'
                    ' expected format "ssh://user@hostname/path_to_socket".')
            remote = urlparse(kwargs['remote_uri'])
            if remote.username is None:
                raise ValueError(
                    'username is required for remote_uri,'
                    ' expected format "ssh://user@hostname/path_to_socket".')
            if remote.path == '':
                raise ValueError(
                    'path is required for remote_uri,'
                    ' expected format "ssh://user@hostname/path_to_socket".')
            if remote.hostname is None:
                raise ValueError(
                    'hostname is required for remote_uri,'
                    ' expected format "ssh://user@hostname/path_to_socket".')

            if kwargs.get('identity_file') is None:
                raise ValueError('identity_file is required.')

            if not os.path.isfile(kwargs['identity_file']):
                raise FileNotFoundError(
                    errno.ENOENT,
                    os.strerror(errno.ENOENT),
                    kwargs['identity_file'],
                )

            return RemoteClient(
                Context(uri, interface, local_path, remote.path,
                        remote.username, remote.hostname,
                        kwargs['identity_file']))
        else:
            return LocalClient(
                Context(uri, interface, None, None, None, None, None))


class LocalClient(BaseClient):
    """Context manager for API workers to access varlink."""

    def __init__(self, context):
        """Construct LocalClient."""
        self._context = context

    def __enter__(self):
        """Enter context for LocalClient."""
        self._client = VarlinkClient(address=self._context.uri)
        self._iface = self._client.open(self._context.interface)
        return self._iface

    def __exit__(self, e_type, e, e_traceback):
        """Cleanup context for LocalClient."""
        if hasattr(self._client, 'close'):
            self._client.close()
        self._iface.close()

        if isinstance(e, VarlinkError):
            raise error_factory(e)


class RemoteClient(BaseClient):
    """Context manager for API workers to access remote varlink."""

    def __init__(self, context):
        """Construct RemoteCLient."""
        self._context = context
        self._portal = Portal()

    def __enter__(self):
        """Context manager for API workers to access varlink."""
        tunnel = self._portal.get(self._context.uri)
        if tunnel is None:
            tunnel = Tunnel(self._context).bore(self._context.uri)
            self._portal[self._context.uri] = tunnel

        try:
            self._client = VarlinkClient(address=self._context.uri)
            self._iface = self._client.open(self._context.interface)
            return self._iface
        except Exception:
            tunnel.close(self._context.uri)
            raise

    def __exit__(self, e_type, e, e_traceback):
        """Cleanup context for RemoteClient."""
        if hasattr(self._client, 'close'):
            self._client.close()
        self._iface.close()

        # set timer to shutdown ssh tunnel
        if isinstance(e, VarlinkError):
            raise error_factory(e)


class Client(object):
    """A client for communicating with a Podman varlink service.

    Example:

        >>> import podman
        >>> c = podman.Client()
        >>> c.system.versions

    Example remote podman:

        >>> import podman
        >>> c = podman.Client(uri='unix:/tmp/podman.sock',
                              remote_uri='ssh://user@host/run/podman/io.projectatomic.podman',
                              identity_file='~/.ssh/id_rsa')
    """

    def __init__(self,
                 uri='unix:/run/podman/io.projectatomic.podman',
                 interface='io.projectatomic.podman',
                 **kwargs):
        """Construct a podman varlink Client.

        uri from default systemd unit file.
        interface from io.projectatomic.podman.varlink, do not change unless
            you are a varlink guru.
        """
        self._client = BaseClient.factory(uri, interface, **kwargs)

        address = "{}-{}".format(uri, interface)
        # Quick validation of connection data provided
        try:
            if not System(self._client).ping():
                raise ConnectionRefusedError(
                    errno.ECONNREFUSED,
                    'Failed varlink connection "{}"'.format(address), address)
        except FileNotFoundError:
            raise ConnectionError(
                errno.ECONNREFUSED,
                ('Failed varlink connection "{}".'
                 ' Is podman service running?').format(address), address)

    def __enter__(self):
        """Return `self` upon entering the runtime context."""
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        """Raise any exception triggered within the runtime context."""
        return None

    @cached_property
    def system(self):
        """Manage system model for podman."""
        return System(self._client)

    @cached_property
    def images(self):
        """Manage images model for libpod."""
        return Images(self._client)

    @cached_property
    def containers(self):
        """Manage containers model for libpod."""
        return Containers(self._client)