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. "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 "repair" will attempt to repair the state of a request that has been
corrupted. corrupted.
@ -234,7 +247,10 @@ def do_staging(self, subcmd, opts, *args):
Use the --cleanup flag to include all obsolete requests. 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 "rebuild" will rebuild broken packages in the given stagings or all
The rebuild command will only trigger builds for packages with less than 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 ignore [-m MESSAGE] REQUEST...
osc staging unignore [--cleanup] [REQUEST...|all] osc staging unignore [--cleanup] [REQUEST...|all]
osc staging list [--supersede] osc staging list [--supersede]
osc staging lock [-m MESSAGE]
osc staging select [--no-freeze] [--move [--from STAGING]] osc staging select [--no-freeze] [--move [--from STAGING]]
[--add PACKAGE] [--add PACKAGE]
STAGING REQUEST... STAGING REQUEST...
@ -302,6 +319,8 @@ def do_staging(self, subcmd, opts, *args):
min_args, max_args = 0, None min_args, max_args = 0, None
elif cmd in ('cleanup_rings', 'acheck'): elif cmd in ('cleanup_rings', 'acheck'):
min_args, max_args = 0, 0 min_args, max_args = 0, 0
elif cmd == 'lock':
min_args, max_args = 0, 0
elif cmd == 'unlock': elif cmd == 'unlock':
min_args, max_args = 0, 0 min_args, max_args = 0, 0
elif cmd == 'rebuild': elif cmd == 'rebuild':
@ -336,7 +355,7 @@ def do_staging(self, subcmd, opts, *args):
lock = OBSLock(opts.apiurl, opts.project, reason=cmd) lock = OBSLock(opts.apiurl, opts.project, reason=cmd)
if cmd == 'unlock': if cmd == 'unlock':
lock.release() lock.release(force=True)
return return
with lock: with lock:
@ -543,6 +562,8 @@ def do_staging(self, subcmd, opts, *args):
UnignoreCommand(api).perform(args[1:], opts.cleanup) UnignoreCommand(api).perform(args[1:], opts.cleanup)
elif cmd == 'list': elif cmd == 'list':
ListCommand(api).perform(supersede=opts.supersede) ListCommand(api).perform(supersede=opts.supersede)
elif cmd == 'lock':
lock.hold(opts.message)
elif cmd == 'adi': elif cmd == 'adi':
AdiCommand(api).perform(args[1:], move=opts.move, by_dp=opts.by_develproject, split=opts.split) AdiCommand(api).perform(args[1:], move=opts.move, by_dp=opts.by_develproject, split=opts.split)
elif cmd == 'rebuild': elif cmd == 'rebuild':

View File

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