2015-02-10 17:22:00 +01:00
|
|
|
from datetime import datetime
|
|
|
|
import time
|
2015-02-19 10:57:55 +01:00
|
|
|
import warnings
|
2022-02-18 10:16:17 +01:00
|
|
|
from lxml import etree as ET
|
2015-02-10 17:22:00 +01:00
|
|
|
|
|
|
|
from osc import conf
|
|
|
|
from osc.core import makeurl
|
|
|
|
from osc.core import http_GET
|
|
|
|
from osc.core import http_POST
|
|
|
|
|
2022-02-18 10:16:17 +01:00
|
|
|
from urllib.error import HTTPError
|
2015-02-10 17:22:00 +01:00
|
|
|
|
2022-02-18 17:15:48 +01:00
|
|
|
|
2015-02-10 17:22:00 +01:00
|
|
|
class OBSLock(object):
|
|
|
|
"""Implement a distributed lock using a shared OBS resource."""
|
|
|
|
|
2017-05-08 19:39:15 -05:00
|
|
|
def __init__(self, apiurl, project, ttl=3600, reason=None, needed=True):
|
2015-02-10 17:22:00 +01:00
|
|
|
self.apiurl = apiurl
|
|
|
|
self.project = project
|
2015-02-19 10:57:55 +01:00
|
|
|
self.lock = conf.config[project]['lock']
|
|
|
|
self.ns = conf.config[project]['lock-ns']
|
2015-02-10 17:22:00 +01:00
|
|
|
# TTL is measured in seconds
|
|
|
|
self.ttl = ttl
|
|
|
|
self.user = conf.config['api_host_options'][apiurl]['user']
|
2017-04-28 16:09:34 -05:00
|
|
|
self.reason = reason
|
2017-04-28 17:03:13 -05:00
|
|
|
self.reason_sub = None
|
2015-02-10 17:22:00 +01:00
|
|
|
self.locked = False
|
2017-05-08 19:39:15 -05:00
|
|
|
self.needed = needed
|
2015-02-10 17:22:00 +01:00
|
|
|
|
|
|
|
def _signature(self):
|
|
|
|
"""Create a signature with a timestamp."""
|
2017-04-28 17:03:13 -05:00
|
|
|
reason = str(self.reason)
|
|
|
|
if self.reason_sub:
|
|
|
|
reason += ' ({})'.format(self.reason_sub)
|
|
|
|
reason = reason.replace('@', 'at').replace('#', 'hash')
|
2019-04-24 08:46:54 +02:00
|
|
|
return '%s#%s@%s' % (self.user, reason, datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%f'))
|
2015-02-10 17:22:00 +01:00
|
|
|
|
|
|
|
def _parse(self, signature):
|
|
|
|
"""Parse a signature into an user and a timestamp."""
|
2017-04-28 17:03:13 -05:00
|
|
|
user, reason, reason_sub, ts = None, None, None, None
|
2015-02-10 17:22:00 +01:00
|
|
|
try:
|
2017-04-28 16:09:34 -05:00
|
|
|
rest, ts_str = signature.split('@')
|
|
|
|
user, reason = rest.split('#')
|
2017-04-28 17:03:13 -05:00
|
|
|
if ' (hold' in reason:
|
|
|
|
reason, reason_sub = reason.split(' (', 1)
|
|
|
|
reason_sub = reason_sub.rstrip(')')
|
2015-02-10 17:22:00 +01:00
|
|
|
ts = datetime.strptime(ts_str, '%Y-%m-%dT%H:%M:%S.%f')
|
|
|
|
except (AttributeError, ValueError):
|
|
|
|
pass
|
2017-04-28 17:03:13 -05:00
|
|
|
return user, reason, reason_sub, ts
|
2015-02-10 17:22:00 +01:00
|
|
|
|
|
|
|
def _read(self):
|
2015-02-19 10:57:55 +01:00
|
|
|
url = makeurl(self.apiurl, ['source', self.lock, '_attribute', '%s:LockedBy' % self.ns])
|
2019-04-21 12:00:26 +02:00
|
|
|
try:
|
|
|
|
root = ET.parse(http_GET(url)).getroot()
|
|
|
|
except HTTPError as e:
|
|
|
|
if e.code == 404:
|
|
|
|
return None
|
|
|
|
raise e
|
2015-02-10 17:22:00 +01:00
|
|
|
signature = None
|
|
|
|
try:
|
|
|
|
signature = root.find('.//value').text
|
|
|
|
except (AttributeError, ValueError):
|
|
|
|
pass
|
|
|
|
return signature
|
|
|
|
|
|
|
|
def _write(self, signature):
|
2018-06-15 08:41:24 +02:00
|
|
|
url = makeurl(self.apiurl, ['source', self.lock, '_attribute'])
|
2015-02-10 17:22:00 +01:00
|
|
|
data = """
|
|
|
|
<attributes>
|
2015-02-19 10:57:55 +01:00
|
|
|
<attribute namespace='%s' name='LockedBy'>
|
2015-02-10 17:22:00 +01:00
|
|
|
<value>%s</value>
|
|
|
|
</attribute>
|
2015-02-19 10:57:55 +01:00
|
|
|
</attributes>""" % (self.ns, signature)
|
2015-02-10 17:22:00 +01:00
|
|
|
http_POST(url, data=data)
|
|
|
|
|
|
|
|
def acquire(self):
|
2022-02-18 13:42:57 +01:00
|
|
|
if not self.needed:
|
|
|
|
return self
|
2017-05-08 19:39:15 -05:00
|
|
|
|
2015-02-19 10:57:55 +01:00
|
|
|
# If the project doesn't have locks configured, raise a
|
|
|
|
# Warning (but continue the operation)
|
|
|
|
if not self.lock:
|
|
|
|
warnings.warn('Locking attribute is not found. Create one to avoid race conditions.')
|
|
|
|
return self
|
|
|
|
|
2017-04-28 17:03:13 -05:00
|
|
|
user, reason, reason_sub, ts = self._parse(self._read())
|
2015-02-10 17:22:00 +01:00
|
|
|
if user and ts:
|
|
|
|
now = datetime.utcnow()
|
|
|
|
if now < ts:
|
|
|
|
raise Exception('Lock acquired from the future [%s] by [%s]. Try later.' % (ts, user))
|
2017-04-06 17:37:55 -05:00
|
|
|
delta = now - ts
|
2017-05-03 20:38:09 -05:00
|
|
|
if delta.total_seconds() < self.ttl:
|
2017-05-03 20:37:03 -05:00
|
|
|
# Existing lock that has not expired.
|
|
|
|
stop = True
|
|
|
|
if user == self.user:
|
|
|
|
if reason.startswith('hold'):
|
|
|
|
# Command being issued during a hold.
|
|
|
|
self.reason_sub = reason
|
|
|
|
stop = False
|
|
|
|
elif reason == 'lock':
|
|
|
|
# Second pass to acquire hold.
|
|
|
|
stop = False
|
|
|
|
|
|
|
|
if stop:
|
2018-11-05 15:46:50 +01:00
|
|
|
print('Lock acquired by [%s] %s ago, reason <%s>. Try later.' % (user, delta, reason))
|
2017-05-03 20:37:03 -05:00
|
|
|
exit(-1)
|
2015-02-10 17:22:00 +01:00
|
|
|
self._write(self._signature())
|
|
|
|
|
|
|
|
time.sleep(1)
|
2017-04-28 17:03:13 -05:00
|
|
|
user, _, _, _ = self._parse(self._read())
|
2015-02-10 17:22:00 +01:00
|
|
|
if user != self.user:
|
|
|
|
raise Exception('Race condition, [%s] wins. Try later.' % user)
|
2017-05-01 17:09:43 -05:00
|
|
|
self.locked = True
|
2015-02-10 17:22:00 +01:00
|
|
|
|
|
|
|
return self
|
|
|
|
|
2017-04-28 17:03:13 -05:00
|
|
|
def release(self, force=False):
|
2022-02-18 13:42:57 +01:00
|
|
|
if not force and not self.needed:
|
|
|
|
return
|
2017-05-08 19:39:15 -05:00
|
|
|
|
2015-02-19 10:57:55 +01:00
|
|
|
# If the project do not have locks configured, simply ignore
|
|
|
|
# the operation.
|
|
|
|
if not self.lock:
|
|
|
|
return
|
|
|
|
|
2017-04-28 17:03:13 -05:00
|
|
|
user, reason, reason_sub, _ = self._parse(self._read())
|
2017-05-08 19:57:04 -05:00
|
|
|
clear = False
|
2015-02-10 17:22:00 +01:00
|
|
|
if user == self.user:
|
2017-04-28 17:03:13 -05:00
|
|
|
if reason_sub:
|
|
|
|
self.reason = reason_sub
|
|
|
|
self.reason_sub = None
|
|
|
|
self._write(self._signature())
|
|
|
|
elif not reason.startswith('hold') or force:
|
2017-05-08 19:57:04 -05:00
|
|
|
# Only clear a command lock as hold can only be cleared by force.
|
|
|
|
clear = True
|
|
|
|
elif user is not None and force:
|
|
|
|
# Clear if a lock is present and force.
|
|
|
|
clear = True
|
|
|
|
|
|
|
|
if clear:
|
|
|
|
self._write('')
|
|
|
|
self.locked = False
|
2017-04-28 17:03:13 -05:00
|
|
|
|
|
|
|
def hold(self, message=None):
|
|
|
|
self.reason = 'hold'
|
|
|
|
if message:
|
|
|
|
self.reason += ': ' + message
|
|
|
|
self.acquire()
|
2015-02-10 17:22:00 +01:00
|
|
|
|
|
|
|
__enter__ = acquire
|
|
|
|
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
|
|
self.release()
|