diff --git a/osc-staging.py b/osc-staging.py index 0a90b8aa..ad22e3c5 100644 --- a/osc-staging.py +++ b/osc-staging.py @@ -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': diff --git a/osclib/obslock.py b/osclib/obslock.py index 5df96473..abf14615 100644 --- a/osclib/obslock.py +++ b/osclib/obslock.py @@ -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