lock: provide command to place a multi-command hold on a project.
This commit is contained in:
parent
77f79f9175
commit
53a79f2c05
@ -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':
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user