Skip to content

Commit

Permalink
Add context manager to ipalib.API
Browse files Browse the repository at this point in the history
`ipalib.API` instances like `ipalib.api` now provide a context manager
that connects and disconnects the API object. Users no longer have to
deal with different types of backends or finalize the API correctly.

```python
import ipalib

with ipalib.api as api:
    api.Commands.ping()
```

See: https://pagure.io/freeipa/issue/9443
Signed-off-by: Christian Heimes <cheimes@redhat.com>
Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
  • Loading branch information
tiran authored and flo-renaud committed Sep 29, 2023
1 parent 8b70ee1 commit 6aebfe7
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 0 deletions.
62 changes: 62 additions & 0 deletions ipalib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -936,6 +936,68 @@ def _enable_warnings(error=False):
class API(plugable.API):
bases = (Command, Object, Method, Backend, Updater)

def __enter__(self):
"""Context manager for IPA API
The context manager connects the backend connect on enter and
disconnects on exit. The process must have access to a valid Kerberos
ticket or have automatic authentication with a keytab or gssproxy
set up. The connection type depends on ``in_server`` and ``context``
options. Server connections use LDAP while clients use JSON-RPC over
HTTPS.
The context manager also finalizes the API object, in case it hasn't
been finalized yet. It is possible to use a custom API object. In
that case, the global API object must be finalized, first. Some
options like logging only apply to global ``ipalib.api`` object.
Usage with global api object::
import os
import ipalib
# optional: automatic authentication with a KRB5 keytab
os.environ.update(
KRB5_CLIENT_KTNAME="/path/to/service.keytab",
KRB5RCACHENAME="FILE:/path/to/tmp/service.ccache",
)
# optional: override settings (once per process)
overrides = {}
ipalib.api.bootstrap(**overrides)
with ipalib.api as api:
host = api.Command.host_show(api.env.host)
user = api.Command.user_show("admin")
"""
# Several IPA module require api.env at import time, some even
# a fully finalized ipalib.ap, e.g. register() with MethodOverride.
if self is not api and not api.isdone("finalize"):
raise RuntimeError("global ipalib.api must be finalized first.")
# initialize this api
if not self.isdone("finalize"):
self.finalize()
# connect backend, server and client use different backends.
if self.env.in_server:
conn = self.Backend.ldap2
else:
conn = self.Backend.rpcclient
if conn.isconnected():
raise RuntimeError("API is already connected")
else:
conn.connect()
return self

def __exit__(self, exc_type, exc_val, exc_tb):
"""Disconnect backend on exit"""
if self.env.in_server:
conn = self.Backend.ldap2
else:
conn = self.Backend.rpcclient
if conn.isconnected():
conn.disconnect()

@property
def packages(self):
if self.env.in_server:
Expand Down
19 changes: 19 additions & 0 deletions ipatests/test_integration/example_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import os

import ipalib
from ipaplatform.paths import paths

# authenticate with host keytab and custom ccache
os.environ.update(
KRB5_CLIENT_KTNAME=paths.KRB5_KEYTAB,
)

# custom options
overrides = {"context": "example_cli"}
ipalib.api.bootstrap(**overrides)

with ipalib.api as api:
user = api.Command.user_show("admin")
print(user)

assert not api.Backend.rpcclient.isconnected()
29 changes: 29 additions & 0 deletions ipatests/test_integration/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import shlex
import ssl
from itertools import chain, repeat
import sys
import textwrap
import time
import pytest
Expand Down Expand Up @@ -1557,6 +1558,34 @@ def test_ipa_getkeytab_server(self):

assert 'Discovered server %s' % self.master.hostname in result

def test_ipa_context_manager(self):
"""Exercise ipalib.api context manager and KRB5_CLIENT_KTNAME auth
The example_cli.py script uses the context manager to connect and
disconnect the global ipalib.api object. The test also checks whether
KRB5_CLIENT_KTNAME env var automatically acquires a TGT.
"""
host = self.clients[0]
tasks.kdestroy_all(host)

here = os.path.abspath(os.path.dirname(__file__))
with open(os.path.join(here, "example_cli.py")) as f:
contents = f.read()

# upload script and run with Python executable
script = "/tmp/example_cli.py"
host.put_file_contents(script, contents)
result = host.run_command([sys.executable, script])

# script prints admin account
admin_princ = f"admin@{host.domain.realm}"
assert admin_princ in result.stdout_text

# verify that auto-login did use correct principal
host_princ = f"host/{host.hostname}@{host.domain.realm}"
result = host.run_command([paths.KLIST])
assert host_princ in result.stdout_text


class TestIPACommandWithoutReplica(IntegrationTest):
"""
Expand Down

0 comments on commit 6aebfe7

Please sign in to comment.