lock: provide command to place a multi-command hold on a project.

This commit is contained in:
Jimmy Berry 2017-04-28 17:03:13 -05:00
parent 77f79f9175
commit 53a79f2c05
2 changed files with 53 additions and 11 deletions

View File

@ -157,6 +157,19 @@ def do_staging(self, subcmd, opts, *args):
"list" will list/supersede requests for ring packages or all if no rings.
"lock" acquire a hold on the project in order to execute multiple commands
and prevent others from interrupting. An example:
lock -m "checkin round"
list --supersede
adi
accept A B C D E
unlock
Each command will update the lock to keep it up-to-date.
"repair" will attempt to repair the state of a request that has been
corrupted.
@ -234,7 +247,10 @@ def do_staging(self, subcmd, opts, *args):
Use the --cleanup flag to include all obsolete requests.
"unlock" will remove the staging lock in case it gets stuck
"unlock" will remove the staging lock in case it gets stuck or a manual hold
If a command lock gets stuck while a hold is placed on a project the
unlock command will need to be run twice since there are two layers of
locks.
"rebuild" will rebuild broken packages in the given stagings or all
The rebuild command will only trigger builds for packages with less than
@ -257,6 +273,7 @@ def do_staging(self, subcmd, opts, *args):
osc staging ignore [-m MESSAGE] REQUEST...
osc staging unignore [--cleanup] [REQUEST...|all]
osc staging list [--supersede]
osc staging lock [-m MESSAGE]
osc staging select [--no-freeze] [--move [--from STAGING]]
[--add PACKAGE]
STAGING REQUEST...
@ -302,6 +319,8 @@ def do_staging(self, subcmd, opts, *args):
min_args, max_args = 0, None
elif cmd in ('cleanup_rings', 'acheck'):
min_args, max_args = 0, 0
elif cmd == 'lock':
min_args, max_args = 0, 0
elif cmd == 'unlock':
min_args, max_args = 0, 0
elif cmd == 'rebuild':
@ -336,7 +355,7 @@ def do_staging(self, subcmd, opts, *args):
lock = OBSLock(opts.apiurl, opts.project, reason=cmd)
if cmd == 'unlock':
lock.release()
lock.release(force=True)
return
with lock:
@ -543,6 +562,8 @@ def do_staging(self, subcmd, opts, *args):
UnignoreCommand(api).perform(args[1:], opts.cleanup)
elif cmd == 'list':
ListCommand(api).perform(supersede=opts.supersede)
elif cmd == 'lock':
lock.hold(opts.message)
elif cmd == 'adi':
AdiCommand(api).perform(args[1:], move=opts.move, by_dp=opts.by_develproject, split=opts.split)
elif cmd == 'rebuild':

View File

@ -37,23 +37,30 @@ class OBSLock(object):
self.ttl = ttl
self.user = conf.config['api_host_options'][apiurl]['user']
self.reason = reason
self.reason_sub = None
self.locked = False
def _signature(self):
"""Create a signature with a timestamp."""
reason = str(self.reason).replace('@', 'at').replace('#', 'hash')
reason = str(self.reason)
if self.reason_sub:
reason += ' ({})'.format(self.reason_sub)
reason = reason.replace('@', 'at').replace('#', 'hash')
return '%s#%s@%s' % (self.user, reason, datetime.isoformat(datetime.utcnow()))
def _parse(self, signature):
"""Parse a signature into an user and a timestamp."""
user, reason, ts = None, None, None
user, reason, reason_sub, ts = None, None, None, None
try:
rest, ts_str = signature.split('@')
user, reason = rest.split('#')
if ' (hold' in reason:
reason, reason_sub = reason.split(' (', 1)
reason_sub = reason_sub.rstrip(')')
ts = datetime.strptime(ts_str, '%Y-%m-%dT%H:%M:%S.%f')
except (AttributeError, ValueError):
pass
return user, reason, ts
return user, reason, reason_sub, ts
def _read(self):
url = makeurl(self.apiurl, ['source', self.lock, '_attribute', '%s:LockedBy' % self.ns])
@ -82,34 +89,48 @@ class OBSLock(object):
warnings.warn('Locking attribute is not found. Create one to avoid race conditions.')
return self
user, reason, ts = self._parse(self._read())
user, reason, reason_sub, ts = self._parse(self._read())
if user and ts:
now = datetime.utcnow()
if now < ts:
raise Exception('Lock acquired from the future [%s] by [%s]. Try later.' % (ts, user))
delta = now - ts
if delta.seconds < self.ttl:
if delta.seconds < self.ttl and not(
user == self.user and (reason == 'lock' or reason.startswith('hold'))):
print 'Lock acquired by [%s] %s ago, reason <%s>. Try later.' % (user, delta, reason)
exit(-1)
# raise Exception('Lock acquired by [%s]. Try later.' % user)
if reason and reason != 'lock':
self.reason_sub = reason
self._write(self._signature())
time.sleep(1)
user, _, _ = self._parse(self._read())
user, _, _, _ = self._parse(self._read())
if user != self.user:
raise Exception('Race condition, [%s] wins. Try later.' % user)
return self
def release(self):
def release(self, force=False):
# If the project do not have locks configured, simply ignore
# the operation.
if not self.lock:
return
user, reason, _ = self._parse(self._read())
user, reason, reason_sub, _ = self._parse(self._read())
if user == self.user:
self._write('')
if reason_sub:
self.reason = reason_sub
self.reason_sub = None
self._write(self._signature())
elif not reason.startswith('hold') or force:
self._write('')
def hold(self, message=None):
self.reason = 'hold'
if message:
self.reason += ': ' + message
self.acquire()
__enter__ = acquire