"""A client for communicating with a Podman varlink service.""" from __future__ import absolute_import import errno import logging 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 from .libs.pods import Pods class BaseClient(): """Context manager for API workers to access varlink.""" def __init__(self, context): """Construct Client.""" self._client = None self._iface = None self._context = context def __call__(self): """Support being called for old API.""" return self @classmethod def factory(cls, uri=None, interface='io.podman', *_args, **kwargs): """Construct a Client based on input.""" # pylint: disable=keyword-arg-before-vararg log_level = os.environ.get('LOG_LEVEL') if log_level is not None: logging.basicConfig(level=logging.getLevelName(log_level.upper())) 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', 'ignore_hosts', 'known_hosts')) 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') is None: return LocalClient(Context(uri, interface)) required = ('{} is required, expected format' ' "ssh://user@hostname[:port]/path_to_socket".') # Remote access requires the full tuple of information if kwargs.get('remote_uri') is None: raise ValueError(required.format('remote_uri')) remote = urlparse(kwargs['remote_uri']) if remote.username is None: raise ValueError(required.format('username')) if remote.path == '': raise ValueError(required.format('path')) if remote.hostname is None: raise ValueError(required.format('hostname')) return RemoteClient( Context( uri, interface, local_path, remote.path, remote.username, remote.hostname, remote.port, kwargs.get('identity_file'), kwargs.get('ignore_hosts'), kwargs.get('known_hosts'), )) class LocalClient(BaseClient): """Context manager for API workers to access varlink.""" 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'): # pylint: disable=no-member 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.""" super().__init__(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._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() raise def __exit__(self, e_type, e, e_traceback): """Cleanup context for RemoteClient.""" if hasattr(self._client, 'close'): # pylint: disable=no-member self._client.close() self._iface.close() # set timer to shutdown ssh tunnel # self._portal.get(self._context.uri).close() if isinstance(e, VarlinkError): raise error_factory(e) class Client(): """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.podman', identity_file='~/.ssh/id_rsa') """ def __init__(self, uri='unix:/run/podman/io.podman', interface='io.podman', **kwargs): """Construct a podman varlink Client. uri from default systemd unit file. interface from io.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)) except FileNotFoundError as ex: raise ConnectionError( errno.ECONNREFUSED, ('Failed varlink connection "{}".' ' Is podman socket or service running?' ).format(address)) from ex 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.""" @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) @cached_property def pods(self): """Manage pods model for libpod.""" return Pods(self._client)