Skip to content
This repository was archived by the owner on Jun 26, 2025. It is now read-only.

Commit 7f703d5

Browse files
authored
Merge pull request #28 from ksauzz/feature/none-per-process-ccache
Add additional ticket updaters
2 parents 3f7e1b5 + 225aa8a commit 7f703d5

File tree

12 files changed

+302
-104
lines changed

12 files changed

+302
-104
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,21 @@ ticket.updater_start()
1717

1818
If `keytab path` is not specifyed, kinit uses `KRB5_KTNAME` env, or `/etc/krb5.keytab` to find a keytab file. see: kerberos(1) and kinit(1).
1919

20+
### Ticket Updater Strategies
21+
22+
To avoid a credential cache (ccache) corruption by concurrent updates from multiple processes, KrbTicketUpdater has a few update strategies:
23+
24+
- SimpleKrbTicketUpdater: for single updater process, or multiple updaters w/ per process ccache. (default)
25+
- MultiProcessKrbTicketUpdater: for multiple updater processes w/ exclusive file lock
26+
- SingleProcessKrbTicketUpdater: for multiple updater processes w/ exclusive file lock to restrict the number of updater processes to one against the ccache
27+
28+
```
29+
from krbticket import KrbTicket, SingleProcessKrbTicketUpdater
30+
31+
ticket = KrbTicket.init("<principal>", "<keytab path>", updater_class=SingleProcessKrbTicketUpdater)
32+
ticket.updater_start()
33+
```
34+
2035
### Retry
2136

2237
krbticket supports retry feature utilizing [retrying](https://github.com/rholder/retrying) which provides various retry strategy. To change the behavior, pass the options using `retry_options` of KrbConfig. The dafault values are:

krbticket/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
from krbticket.ticket import *
2+
from krbticket.updater import *
23
from krbticket.config import *

krbticket/command.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
logger = logging.getLogger(__name__)
77

8+
89
class KrbCommand():
910
@staticmethod
1011
def kinit(config):

krbticket/config.py

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@
77

88

99
class KrbConfig():
10+
from krbticket.updater import SimpleKrbTicketUpdater
11+
1012
def __init__(self, principal=None, keytab=None, kinit_bin="kinit",
1113
klist_bin="klist", kdestroy_bin="kdestroy",
1214
renewal_threshold=timedelta(minutes=30),
1315
ticket_lifetime=None,
1416
ticket_renewable_lifetime=None,
1517
ccache_name=None,
18+
updater_class=SimpleKrbTicketUpdater,
1619
retry_options={
1720
'wait_exponential_multiplier': 1000,
1821
'wait_exponential_max': 30000,
@@ -25,26 +28,47 @@ def __init__(self, principal=None, keytab=None, kinit_bin="kinit",
2528
self.renewal_threshold = renewal_threshold
2629
self.ticket_lifetime = ticket_lifetime
2730
self.ticket_renewable_lifetime = ticket_renewable_lifetime
31+
self.updater_class = updater_class
2832
self.retry_options = retry_options
2933
self.ccache_name = ccache_name if ccache_name else self._ccache_name()
3034

31-
3235
def __str__(self):
3336
super_str = super(KrbConfig, self).__str__()
3437
return "{}: principal={}, keytab={}, kinit_bin={}," \
3538
" klist_bin={}, kdestroy_bin={}, " \
3639
" renewal_threshold={}, ticket_lifetime={}, " \
3740
" ticket_renewable_lifetime={}, " \
3841
" retry_options={}, ccache_name={}, " \
39-
.format(super_str, self.principal, self.keytab, self.kinit_bin, self.klist_bin, self.kdestroy_bin, self.renewal_threshold, self.ticket_lifetime, self.ticket_renewable_lifetime, self.retry_options, self.ccache_name)
40-
42+
" updater_class={}" \
43+
.format(super_str, self.principal, self.keytab, self.kinit_bin,
44+
self.klist_bin, self.kdestroy_bin,
45+
self.renewal_threshold, self.ticket_lifetime,
46+
self.ticket_renewable_lifetime,
47+
self.retry_options, self.ccache_name,
48+
self.updater_class)
4149

4250
def _ccache_name(self):
43-
if multiprocessing.current_process().name == 'MainProcess':
44-
return os.environ.get('KRB5CCNAME', '/tmp/krb5cc_{}'.format(os.getuid()));
51+
if self.updater_class.use_per_process_ccache():
52+
return self._per_process_ccache_name()
53+
else:
54+
return self._default_ccache_name()
55+
56+
def _is_main_process(self):
57+
return multiprocessing.current_process().name == 'MainProcess'
58+
59+
def _default_ccache_name(self):
60+
return os.environ.get('KRB5CCNAME', '/tmp/krb5cc_{}'.format(os.getuid()))
61+
62+
@property
63+
def ccache_lockfile(self):
64+
return '{}.krbticket.lock'.format(self.ccache_name)
65+
66+
def _per_process_ccache_name(self):
67+
if self._is_main_process():
68+
return self._default_ccache_name()
4569

46-
# For multiprocess application. e.g. gunicorn
4770
new_ccname = "/tmp/krb5cc_{}_{}".format(os.getuid(), os.getpid())
71+
# Update KRB5CCNAME for kinit
4872
os.environ['KRB5CCNAME'] = new_ccname
4973
logger.info("env KRB5CCNAME is updated to '{}' for multiprocessing".format(new_ccname))
5074

krbticket/ticket.py

Lines changed: 3 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,18 @@
11
import os
22
from datetime import datetime
33
import logging
4-
import threading
5-
import time
64

75
from krbticket.command import KrbCommand
86
from krbticket.config import KrbConfig
7+
from krbticket.updater import KrbTicketUpdater
98

109
logger = logging.getLogger(__name__)
1110

11+
1212
class NoCredentialFound(Exception):
1313
pass
1414

1515

16-
class KrbTicketUpdater(threading.Thread):
17-
DEFAULT_INTERVAL = 60 * 10
18-
19-
def __init__(self, ticket, interval=DEFAULT_INTERVAL):
20-
super(KrbTicketUpdater, self).__init__()
21-
22-
self.ticket = ticket
23-
self.interval = interval
24-
self.stop_event = threading.Event()
25-
self.daemon = True
26-
27-
def run(self):
28-
logger.info("Ticket updater start...")
29-
while True:
30-
if self.stop_event.is_set():
31-
return
32-
33-
logger.debug("Trying to update ticket...")
34-
self.ticket.maybe_update()
35-
time.sleep(self.interval)
36-
37-
def stop(self):
38-
logger.debug("Stopping ticket updater...")
39-
self.stop_event.set()
40-
41-
4216
class KrbTicket():
4317
def __init__(self, config=None, file=None, principal=None, starting=None, expires=None,
4418
service_principal=None, renew_expires=None):
@@ -55,7 +29,7 @@ def updater_start(self, interval=KrbTicketUpdater.DEFAULT_INTERVAL):
5529
self.updater(interval=interval).start()
5630

5731
def updater(self, interval=KrbTicketUpdater.DEFAULT_INTERVAL):
58-
return KrbTicketUpdater(self, interval=interval)
32+
return self.config.updater_class(self, interval=interval)
5933

6034
def maybe_update(self):
6135
self.reload()
@@ -99,15 +73,13 @@ def need_reinit(self):
9973
else:
10074
return self.need_renewal()
10175

102-
10376
def __str__(self):
10477
super_str = super(KrbTicket, self).__str__()
10578
return "{}: file={}, principal={}, starting={}, expires={}," \
10679
" service_principal={}, renew_expires={}" \
10780
.format(super_str, self.file, self.principal, self.starting,
10881
self.expires, self.service_principal, self.renew_expires)
10982

110-
11183
@staticmethod
11284
def cache_exists(config):
11385
return os.path.isfile(config.ccache_name)

krbticket/updater.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import logging
2+
import threading
3+
import time
4+
5+
import fasteners
6+
7+
logger = logging.getLogger(__name__)
8+
9+
10+
class KrbTicketUpdater(threading.Thread):
11+
DEFAULT_INTERVAL = 60 * 10
12+
13+
def __init__(self, ticket, interval=DEFAULT_INTERVAL):
14+
super(KrbTicketUpdater, self).__init__()
15+
16+
self.ticket = ticket
17+
self.interval = interval
18+
self.stop_event = threading.Event()
19+
self.daemon = True
20+
21+
def run(self):
22+
logger.info("{} start...".format(self.__class__.__name__))
23+
while True:
24+
if self.stop_event.is_set():
25+
return
26+
27+
logger.debug("Trying to update ticket...")
28+
self.update()
29+
time.sleep(self.interval)
30+
31+
def update(self):
32+
raise NotImplementedError
33+
34+
@staticmethod
35+
def use_per_process_ccache():
36+
raise NotImplementedError
37+
38+
def stop(self):
39+
logger.debug("Stopping ticket updater...")
40+
self.stop_event.set()
41+
42+
43+
class SimpleKrbTicketUpdater(KrbTicketUpdater):
44+
"""
45+
KrbTicketUpdater w/o exclusion control
46+
47+
Using this with multiprocessing, child processes uses dedicated ccache file
48+
"""
49+
def update(self):
50+
self.ticket.maybe_update()
51+
52+
@staticmethod
53+
def use_per_process_ccache():
54+
return True
55+
56+
57+
class MultiProcessKrbTicketUpdater(KrbTicketUpdater):
58+
"""
59+
Multiprocess KrbTicket Updater
60+
61+
KrbTicketUpdater w/ exclusive lock for a ccache
62+
"""
63+
def update(self):
64+
with fasteners.InterProcessLock(self.ticket.config.ccache_lockfile):
65+
self.ticket.maybe_update()
66+
67+
@staticmethod
68+
def use_per_process_ccache():
69+
return False
70+
71+
72+
class SingleProcessKrbTicketUpdater(KrbTicketUpdater):
73+
"""
74+
Singleprocess KrbTicket Updater
75+
76+
Single Process KrbTicketUpdater on the system.
77+
Multiple updaters can start, but they immediately stops if a updater is already running on the system.
78+
"""
79+
def run(self):
80+
lock = fasteners.InterProcessLock(self.ticket.config.ccache_lockfile)
81+
got_lock = lock.acquire(blocking=False)
82+
if not got_lock:
83+
logger.debug("Another updater is detected. Stopping ticket updater...")
84+
return
85+
86+
logger.debug("Got lock: {}...".format(self.ticket.config.ccache_lockfile))
87+
try:
88+
super().run()
89+
finally:
90+
if got_lock:
91+
lock.release()
92+
logger.debug("Released lock: {}...".format(self.ticket.config.ccache_lockfile))
93+
94+
def update(self):
95+
self.ticket.maybe_update()
96+
97+
@staticmethod
98+
def use_per_process_ccache():
99+
return False

pytest.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22
addopts = -x -v -s --cov=krbticket --cov-report=html
33
log_cli = 1
44
log_cli_level = DEBUG
5-
log_cli_format = %(asctime)s [%(levelname)8s] %(name)s %(message)s (%(filename)s:%(lineno)s)
5+
log_cli_format = %(asctime)s [%(levelname)8s] %(processName)s - %(name)s %(message)s (%(filename)s:%(lineno)s)
66
log_cli_date_format=%Y-%m-%d %H:%M:%S
77

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
retrying==1.3.3
2+
fasteners==0.15

tests/helper.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
DEFAULT_PRINCIPAL = 'user@EXAMPLE.COM'
77
DEFAULT_KEYTAB = './tests/conf/krb5.keytab'
88
DEFAULT_TICKET_RENEWAL_THRESHOLD_SEC = 1
9-
DEFAULT_TICKET_LIFETIME = '2s'
10-
DEFAULT_TICKET_RENEWABLE_LIFETIME = '4s'
9+
DEFAULT_TICKET_LIFETIME_SEC = 4
10+
DEFAULT_TICKET_LIFETIME = '{}s'.format(DEFAULT_TICKET_LIFETIME_SEC)
11+
DEFAULT_TICKET_RENEWABLE_LIFETIME_SEC = 8
12+
DEFAULT_TICKET_RENEWABLE_LIFETIME = '{}s'.format(DEFAULT_TICKET_RENEWABLE_LIFETIME_SEC)
1113
DEFAULT_CCACHE_NAME = '/tmp/krb5cc_{}'.format(os.getuid())
1214

1315

@@ -33,17 +35,20 @@ def assert_config(c1, c2):
3335
assert c1.ticket_renewable_lifetime == c2.ticket_renewable_lifetime
3436
assert c1.ccache_name == c2.ccache_name
3537

36-
def default_config():
37-
return KrbConfig(
38-
DEFAULT_PRINCIPAL,
39-
DEFAULT_KEYTAB,
40-
renewal_threshold=timedelta(seconds=DEFAULT_TICKET_RENEWAL_THRESHOLD_SEC),
41-
ticket_lifetime=DEFAULT_TICKET_LIFETIME,
42-
ticket_renewable_lifetime=DEFAULT_TICKET_RENEWABLE_LIFETIME,
43-
retry_options={
38+
39+
def default_config(**kwargs):
40+
default_options = {
41+
'principal': DEFAULT_PRINCIPAL,
42+
'keytab': DEFAULT_KEYTAB,
43+
'renewal_threshold': timedelta(seconds=DEFAULT_TICKET_RENEWAL_THRESHOLD_SEC),
44+
'ticket_lifetime': DEFAULT_TICKET_LIFETIME,
45+
'ticket_renewable_lifetime': DEFAULT_TICKET_RENEWABLE_LIFETIME,
46+
'retry_options': {
4447
'wait_exponential_multiplier': 100,
4548
'wait_exponential_max': 1000,
46-
'stop_max_attempt_number': 3})
49+
'stop_max_attempt_number': 3}
50+
}
51+
return KrbConfig(**{**default_options, **kwargs})
4752

4853

4954
@pytest.fixture

0 commit comments

Comments
 (0)