forked from openSUSE/openSUSE-release-tools
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathleaper.py
executable file
·627 lines (538 loc) · 29.3 KB
/
leaper.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
#!/usr/bin/python3
from pprint import pprint
import os
import sys
import re
import logging
from optparse import OptionParser
import cmdln
try:
from xml.etree import cElementTree as ET
except ImportError:
import cElementTree as ET
import osc.core
from osclib.conf import Config
from osclib.core import devel_project_get
from urllib.error import HTTPError
import yaml
import ReviewBot
from check_source_in_factory import FactorySourceChecker
class Leaper(ReviewBot.ReviewBot):
def __init__(self, *args, **kwargs):
ReviewBot.ReviewBot.__init__(self, *args, **kwargs)
# ReviewBot options.
self.request_default_return = True
self.comment_handler = True
self.do_comments = True
# for FactorySourceChecker
self.factory = FactorySourceChecker(*args, **kwargs)
self.needs_legal_review = False
self.needs_reviewteam = False
self.pending_factory_submission = False
self.source_in_factory = None
self.needs_release_manager = False
self.release_manager_group = None
self.review_team_group = None
self.legal_review_group = None
self.must_approve_version_updates = False
self.must_approve_maintenance_updates = False
self.needs_check_source = False
self.check_source_group = None
self.automatic_submission = False
# project => package list
self.packages = {}
def prepare_review(self):
# update lookup information on every run
self.lookup.reset()
def get_source_packages(self, project, expand=False):
"""Return the list of packages in a project."""
query = {'expand': 1} if expand else {}
try:
root = ET.parse(osc.core.http_GET(osc.core.makeurl(self.apiurl, ['source', project],
query=query))).getroot()
packages = [i.get('name') for i in root.findall('entry')]
except HTTPError as e:
# in case the project doesn't exist yet (like sle update)
if e.code != 404:
raise e
packages = []
return packages
def is_package_in_project(self, project, package):
if not project in self.packages:
self.packages[project] = self.get_source_packages(project)
return True if package in self.packages[project] else False
# this is so bad
def _webui_from_api(self, apiurl):
return apiurl.replace('//api.', '//build.')
# OBS is not smart enough to redirect to the mapped instance and just
# displays 404 so we have to do it ourselves
def package_link(self, project, package):
apiurl = self.apiurl
for prefix, url, srprefix in self.config.project_namespace_api_map:
if project.startswith(prefix):
apiurl = url
project = project[len(prefix):]
break
return '[%(project)s/%(package)s](%(url)s/package/show/%(project)s/%(package)s)' % {
'url': self._webui_from_api(apiurl),
'project': project,
'package': package,
}
def rdiff_link(self, src_project, src_package, src_rev, target_project, target_package = None):
if target_package is None:
target_package = src_package
apiurl = self.apiurl
for prefix, url, srprefix in self.config.project_namespace_api_map:
# can't rdiff if both sides are remote
# https://github.com/openSUSE/open-build-service/issues/4601
if target_project.startswith(prefix) and src_project.startswith(prefix):
apiurl = url
src_project = src_project[len(prefix):]
target_project = target_project[len(prefix):]
break
return self.package_link(target_project, target_package) + ' ([diff](%(url)s/package/rdiff/%(src_project)s/%(src_package)s?opackage=%(target_package)s&oproject=%(target_project)s&rev=%(src_rev)s))' % {
'url': self._webui_from_api(apiurl),
'src_project': src_project,
'src_package': src_package,
'src_rev': src_rev,
'target_project': target_project,
'target_package': target_package,
}
def _check_same_origin(self, origin, project):
if origin == 'FORK':
return True
if origin.startswith('Devel;'):
(dummy, origin, dummy) = origin.split(';')
# FIXME: to make the rest of the code easier this should probably check
# if the srcmd5 matches the origin project. That way it doesn't really
# matter from where something got submitted as long as the sources match.
return project.startswith(origin)
def check_source_submission(self, src_project, src_package, src_rev, target_project, target_package):
ret = self.check_source_submission_inner(src_project, src_package, src_rev, target_project, target_package)
# The layout of this code is just plain wrong and awkward. What is
# really desired a "post check source submission" not
# check_one_request() which applies to all action types and all actions
# at once. The follow-ups need the same context that the main method
# determining to flip the switch had. For maintenance incidents the
# ReviewBot code does a fair bit of mangling to the package and project
# values passed in which is not present during check_one_request().
# Currently the only feature used by maintenance is
# do_check_maintainer_review and the rest of the checks apply in the
# single action workflow so this should work fine, but really all the
# processing should be done per-action instead of per-request.
if self.do_check_maintainer_review:
self.devel_project_review_ensure(self.request, target_project, target_package)
return ret
def check_source_submission_inner(self, src_project, src_package, src_rev, target_project, target_package):
super(Leaper, self).check_source_submission(src_project, src_package, src_rev, target_project, target_package)
self.automatic_submission = False
if src_project == target_project and src_package == target_package:
self.logger.info('self submission detected')
self.needs_release_manager = True
return True
src_srcinfo = self.get_sourceinfo(src_project, src_package, src_rev)
package = target_package
origin = self.lookup.get(target_project, package)
origin_same = True
if origin:
origin_same = self._check_same_origin(origin, src_project)
if src_srcinfo is None:
# source package does not exist?
# handle here to avoid crashing on the next line
self.logger.warning("Could not get source info for %s/%s@%s" % (src_project, src_package, src_rev))
return False
if self.ibs and target_project.startswith('SUSE:SLE'):
review_result = None
prj = 'openSUSE.org:openSUSE:Factory'
# True or None (open request) are acceptable for SLE.
in_factory = self._check_factory(package, src_srcinfo, prj)
if in_factory:
review_result = True
self.source_in_factory = True
elif in_factory is None:
self.pending_factory_submission = True
else:
if not self.is_package_in_project(prj, package):
self.logger.info('the package is not in Factory, nor submitted there')
else:
self.logger.info('different sources in {}'.format(self.rdiff_link(src_project, src_package, src_rev, prj, package)))
if review_result == None and not self.pending_factory_submission:
other_projects_to_check = []
m = re.match(r'SUSE:SLE-(\d+)(?:-SP(\d+)):', target_project)
if m:
sle_version = int(m.group(1))
sp_version = int(m.group(2))
versions_to_check = []
# yeah, too much harcoding here
if sle_version == 12:
versions_to_check = [ '42.3' ]
elif sle_version == 15:
versions_to_check = [ '15.%d' % i for i in range(sp_version + 1) ]
else:
self.logger.error("can't handle %d.%d", sle_version, sp_version)
for version in versions_to_check:
leap = 'openSUSE.org:openSUSE:Leap:%s' % (version)
other_projects_to_check += [ leap, leap + ':Update', leap + ':NonFree', leap + ':NonFree:Update' ]
for prj in other_projects_to_check:
if self.is_package_in_project(prj, package):
self.logger.debug('checking {}'.format(prj))
if self._check_factory(package, src_srcinfo, prj) is True:
self.logger.info('found source match in {}'.format(prj))
devel_project, devel_package = devel_project_get(self.apiurl, 'openSUSE.org:openSUSE:Factory', package)
if devel_project is not None:
# specifying devel package is optional
if devel_package is None:
devel_package = package
if self.is_package_in_project(devel_project, devel_package):
if self._check_matching_srcmd5(devel_project, devel_package, src_srcinfo.verifymd5) == True:
self.logger.info('matching sources in {}/{}'.format(devel_project, devel_package))
return True
else:
self.logger.info('different sources in devel project {}'.format(self.rdiff_link(src_project, src_package, src_rev, devel_project, devel_package)))
else:
self.logger.info('no devel project found for {}/{}'.format('openSUSE.org:openSUSE:Factory', package))
self.logger.info('no matching sources found anywhere. Needs a human to decide whether that is ok. Please provide some justification to help that person.')
else:
leap = 'openSUSE.org:openSUSE:Leap:15.1'
if not self.is_package_in_project(target_project, package) \
and self.is_package_in_project(leap, package) \
and self._check_factory(package, src_srcinfo, leap) is False:
self.logger.info('different sources in {}'.format(self.rdiff_link(src_project, src_package, src_rev, leap, package)))
if not review_result and origin is not None:
review_result = origin_same
if origin_same or origin == 'openSUSE.org:openSUSE:Factory' and self.pending_factory_submission:
self.logger.info("ok, origin %s unchanged", origin)
else:
# only log origin state if it's taken into consideration for the review result
self.logger.info("Submitted from a different origin than expected ('%s')", origin)
self.needs_release_manager = True
# no result so far and also no factory submission to wait
# for. So just pass to avoid requring too much overrides
if not self.pending_factory_submission:
review_result = True
if not review_result and self.override_allow:
# Rather than decline, leave review open in-case of change and
# ask release manager for input via override comment.
self.logger.info('Comment `(at){} override accept` to force accept.'.format(self.review_user))
self.needs_release_manager = True
review_result = None
return review_result
if target_project.endswith(':Update'):
self.logger.info("expected origin is '%s' (%s)", origin,
"unchanged" if origin_same else "changed")
# Only when not from current product should request require maintainer review.
self.do_check_maintainer_review = False
if origin_same:
return True
good = self._check_matching_srcmd5(origin, target_package, src_srcinfo.verifymd5)
if good:
self.logger.info('submission source found in origin ({})'.format(origin))
return good
good = self.factory._check_requests(origin, target_package, src_srcinfo.verifymd5)
if good or good == None:
self.logger.info('found pending submission against origin ({})'.format(origin))
return good
# TODO #1662: Uncomment once maintbot has been superseded and leaper
# is no longer run in comment-only mode.
#self.do_check_maintainer_review = True
return None
elif self.action.type == 'maintenance_incident':
self.logger.debug('unhandled incident pattern (targetting non :Update project)')
return True
# obviously
if src_project in ('openSUSE:Factory', 'openSUSE:Factory:NonFree'):
self.source_in_factory = True
is_fine_if_factory = False
not_in_factory_okish = False
if origin:
self.logger.info("expected origin is '%s' (%s)", origin,
"unchanged" if origin_same else "changed")
if origin.startswith('Devel;'):
if origin_same == False:
self.logger.debug("not submitted from devel project")
return False
is_fine_if_factory = True
not_in_factory_okish = True
if self.must_approve_version_updates:
self.needs_release_manager = True
# fall through to check history and requests
elif origin.startswith('openSUSE:Factory'):
# A large number of requests are created by hand that leaper
# would have created via update_crawler.py. This applies to
# other origins, but primary looking to let Factory submitters
# know that there is no need to make manual submissions to both.
# Since it has a lookup entry it is not a new package.
self.automatic_submission = False
if self.must_approve_version_updates:
self.needs_release_manager = True
if origin == src_project:
self.source_in_factory = True
# no need to approve submissions from Factory if
# the lookup file points to Factory. Just causes
# spam for many maintainers #1393
self.do_check_maintainer_review = False
is_fine_if_factory = True
# fall through to check history and requests
elif origin == 'FORK':
is_fine_if_factory = True
if not src_project.startswith('SUSE:SLE-'):
not_in_factory_okish = True
self.needs_check_source = True
self.needs_release_manager = True
# fall through to check history and requests
# TODO Ugly save for 15.1 (n-1).
elif origin.startswith('openSUSE:Leap:15.0'):
if self.must_approve_maintenance_updates:
self.needs_release_manager = True
if src_project.startswith('openSUSE:Leap'):
self.do_check_maintainer_review = False
# submitted from :Update
if origin_same:
self.logger.debug("submission from 15.0 ok")
return True
# switching to sle package might make sense
if src_project.startswith('SUSE:SLE-15'):
self.needs_release_manager = True
self.do_check_maintainer_review = False
return True
# submitted from elsewhere but is in :Update
else:
good = self._check_matching_srcmd5('openSUSE:Leap:15.0:Update', target_package, src_srcinfo.verifymd5)
if good:
self.logger.info("submission found in 15.0")
return good
# check release requests too
good = self.factory._check_requests('openSUSE:Leap:15.0:Update', target_package, src_srcinfo.verifymd5)
if good or good == None:
self.logger.debug("found request")
return good
# let's see where it came from before
oldorigin = self.lookup.get('openSUSE:Leap:15.0', target_package)
if oldorigin:
self.logger.debug("oldorigin {}".format(oldorigin))
# Factory. So it's ok to keep upgrading it to Factory
# TODO: whitelist packages where this is ok and block others?
self.logger.info("Package was from %s in 15.0", oldorigin)
if oldorigin.startswith('openSUSE:Factory'):
# check if an attempt to switch to SLE package is made
for sp in ('SP1:GA', 'SP1:Update'):
good = self._check_matching_srcmd5('SUSE:SLE-15-{}'.format(sp), target_package, src_srcinfo.verifymd5)
if good:
self.logger.info("request sources come from SLE")
self.needs_release_manager = True
return good
# TODO Ugly save for 15.2 (n-2).
elif False and oldorigin.startswith('openSUSE:Leap:15.0'):
self.logger.info("Package was from %s in 15.0", oldorigin)
# the release manager needs to review attempts to upgrade to Factory
is_fine_if_factory = True
self.needs_release_manager = True
elif origin.startswith('SUSE:SLE-15'):
if self.must_approve_maintenance_updates:
self.needs_release_manager = True
if src_project.startswith('SUSE:SLE-15'):
self.do_check_maintainer_review = False
for v in ('15.0', '15.1'):
prj = 'openSUSE:Leap:{}:SLE-workarounds'.format(v)
if self.is_package_in_project( prj, target_package):
self.logger.info("found package in %s", prj)
if not self._check_matching_srcmd5(prj,
target_package,
src_srcinfo.verifymd5):
self.logger.info("sources in %s are NOT identical",
self.rdiff_link(src_project, src_package, src_rev, prj, package))
self.needs_release_manager = True
# submitted from :Update
if origin == src_project:
self.logger.debug("submission origin ok")
return True
elif origin.endswith(':GA') \
and src_project == origin[:-2] + 'Update':
self.logger.debug("sle update submission")
return True
# check if submitted from higher SP
priolist = ['SUSE:SLE-15:', 'SUSE:SLE-15-SP1:', 'SUSE:SLE-15-SP2:', 'SUSE:SLE-15-SP3:']
for i in range(len(priolist) - 1):
if origin.startswith(priolist[i]):
for prj in priolist[i + 1:]:
if src_project.startswith(prj):
self.logger.info("submission from higher service pack %s:* ok", prj)
return True
in_sle_origin = self._check_factory(target_package, src_srcinfo, origin)
if in_sle_origin:
self.logger.info('parallel submission, also in {}'.format(origin))
return True
self.needs_release_manager = True
# the release manager needs to review attempts to upgrade to Factory
is_fine_if_factory = True
else:
self.logger.error("unhandled origin %s", origin)
return False
else: # no origin
# submission from SLE is ok
if src_project.startswith('SUSE:SLE-15'):
self.do_check_maintainer_review = False
return True
# new package submitted from Factory. Check if it was in
# 42.3 before and skip maintainer review if so.
subprj = src_project[len('openSUSE:Factory'):]
# disabled for reference. Needed again for 16.0 probably
if False and self.source_in_factory and target_project.startswith('openSUSE:Leap:15.0') \
and self.is_package_in_project('openSUSE:Leap:42.3' + subprj, package):
self.logger.info('package was in 42.3')
self.do_check_maintainer_review = False
return True
is_fine_if_factory = True
self.needs_release_manager = True
if origin is None or not origin.startswith('SUSE:SLE-'):
for p in (':Update', ':GA'):
prj = 'SUSE:SLE-15' + p
if self.is_package_in_project(prj, package):
self.logger.info('Package is in {}'.format(
self.rdiff_link(src_project, src_package, src_rev, prj, package)))
break
is_in_factory = self.source_in_factory
# we came here because none of the above checks find it good, so
# let's see if the package is in Factory at least
if is_in_factory is None:
is_in_factory = self._check_factory(package, src_srcinfo)
if is_in_factory:
self.source_in_factory = True
self.needs_reviewteam = False
self.needs_legal_review = False
elif is_in_factory is None:
self.pending_factory_submission = True
self.needs_reviewteam = False
self.needs_legal_review = False
else:
if src_project.startswith('SUSE:SLE-15') \
or src_project.startswith('openSUSE:Leap:15.'):
self.needs_reviewteam = False
self.needs_legal_review = False
else:
self.needs_reviewteam = True
self.needs_legal_review = True
self.source_in_factory = False
if is_fine_if_factory:
if self.source_in_factory:
return True
elif self.pending_factory_submission:
return None
elif not_in_factory_okish:
self.needs_reviewteam = True
self.needs_legal_review = True
return True
if self.override_allow:
# Rather than decline, leave review open and ask release
# manager for input via override comment.
self.logger.info('Comment `(at){} override accept` to force accept.'.format(self.review_user))
self.needs_release_manager = True
return None
return False
def _check_factory(self, target_package, src_srcinfo, target_project='openSUSE:Factory'):
for subprj in ('', ':NonFree', ':Live'):
prj = ''.join((target_project, subprj))
good = self._check_matching_srcmd5(prj, target_package, src_srcinfo.verifymd5)
if good:
return good
good = self.factory._check_requests(prj, target_package, src_srcinfo.verifymd5)
if good or good == None:
self.logger.debug("found request to %s", prj)
return good
return False
def _check_project_and_request(self, project, target_package, src_srcinfo):
good = self._check_matching_srcmd5(project, target_package, src_srcinfo.verifymd5)
if good:
return good
good = self.factory._check_requests(project, target_package, src_srcinfo.verifymd5)
if good or good == None:
return good
return False
def check_one_request(self, req):
config = Config.get(self.apiurl, req.actions[0].tgt_project)
self.needs_legal_review = False
self.needs_reviewteam = False
self.needs_release_manager = False
self.pending_factory_submission = False
self.source_in_factory = None
self.do_check_maintainer_review = not self.ibs
self.packages = {}
request_ok = ReviewBot.ReviewBot.check_one_request(self, req)
self.logger.debug("review result: %s", request_ok)
if self.pending_factory_submission:
self.logger.info("submission is waiting for a Factory request to complete")
creator = req.get_creator()
bot_name = self.bot_name.lower()
if self.automatic_submission and creator != bot_name:
self.logger.info('@{}: this request would have been automatically created by {} after the Factory submission was accepted in order to eleviate the need to manually create requests for packages sourced from Factory'.format(creator, bot_name))
elif self.source_in_factory:
self.logger.info("perfect. the submitted sources are in or accepted for Factory")
elif self.source_in_factory == False:
self.logger.warning("the submitted sources are NOT in Factory")
if request_ok == False:
self.logger.info("NOTE: if you think the automated review was wrong here, please talk to the release team before reopening the request")
if self.do_comments:
result = None
if request_ok is None:
state = 'seen'
elif request_ok:
state = 'done'
result = 'accepted'
else:
state = 'done'
result = 'declined'
self.comment_write(state, result)
add_review_groups = []
if self.needs_release_manager:
add_review_groups.append(self.release_manager_group or
config.get(self.override_group_key))
if self.needs_reviewteam:
add_review_groups.append(self.review_team_group or
config.get('review-team'))
if self.needs_legal_review:
add_review_groups.append(self.legal_review_group or
config.get('legal-review-group'))
if self.needs_check_source and self.check_source_group is not None:
add_review_groups.append(self.check_source_group)
for group in add_review_groups:
if group is None:
continue
self.logger.info("{0} needs review by [{1}](/group/show/{1})".format(req.reqid, group))
self.add_review(req, by_group=group)
return request_ok
def check_action__default(self, req, a):
self.needs_release_manager = True
return super(Leaper, self).check_action__default(req, a)
class CommandLineInterface(ReviewBot.CommandLineInterface):
def __init__(self, *args, **kwargs):
ReviewBot.CommandLineInterface.__init__(self, args, kwargs)
self.clazz = Leaper
def get_optparser(self):
parser = ReviewBot.CommandLineInterface.get_optparser(self)
parser.add_option("--no-comment", dest='comment', action="store_false", default=True, help="don't actually post comments to obs")
parser.add_option("--manual-version-updates", action="store_true", help="release manager must approve version updates")
parser.add_option("--manual-maintenance-updates", action="store_true", help="release manager must approve maintenance updates")
parser.add_option("--check-source-group", dest="check_source_group", metavar="GROUP", help="group used by check_source.py bot which will be added as a reviewer should leaper checks pass")
parser.add_option("--review-team-group", dest="review_team_group", metavar="GROUP", help="group used for package reviews")
parser.add_option("--release-manager-group", dest="release_manager_group", metavar="GROUP", help="group used for release manager reviews")
parser.add_option("--legal-review-group", dest="legal_review_group", metavar="GROUP", help="group used for legal reviews")
return parser
def setup_checker(self):
bot = ReviewBot.CommandLineInterface.setup_checker(self)
if self.options.manual_version_updates:
bot.must_approve_version_updates = True
if self.options.manual_maintenance_updates:
bot.must_approve_maintenance_updates = True
if self.options.check_source_group:
bot.check_source_group = self.options.check_source_group
if self.options.review_team_group:
bot.review_team_group = self.options.review_team_group
if self.options.legal_review_group:
bot.legal_review_group = self.options.legal_review_group
if self.options.release_manager_group:
bot.release_manager_group = self.options.release_manager_group
bot.do_comments = self.options.comment
return bot
if __name__ == "__main__":
app = CommandLineInterface()
sys.exit( app.main() )