forked from openSUSE/openSUSE-release-tools
-
Notifications
You must be signed in to change notification settings - Fork 0
/
osc-origin.py
407 lines (333 loc) · 15.5 KB
/
osc-origin.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
from __future__ import print_function
from datetime import timedelta
import json
import logging
import os
import os.path
from osc import cmdln
from osc import core
from osc import oscerr
from osc.core import get_request_list
from osclib.cache import Cache
from osclib.cache_manager import CacheManager
from osclib.core import entity_exists
from osclib.core import package_kind
from osclib.core import package_list
from osclib.core import package_list_kind_filtered
from osclib.core import project_attribute_list
from osclib.core import project_locked
from osclib.origin import config_load
from osclib.origin import config_origin_list
from osclib.origin import origin_find
from osclib.origin import origin_history
from osclib.origin import origin_potentials
from osclib.origin import origin_revision_state
from osclib.origin import origin_updatable
from osclib.origin import origin_updatable_initial
from osclib.origin import origin_update
from osclib.sentry import sentry_init
from osclib.util import mail_send
from shutil import copyfile
import sys
import time
import yaml
OSRT_ORIGIN_LOOKUP_TTL = 60 * 60 * 24 * 7
@cmdln.option('--debug', action='store_true', help='output debug information')
@cmdln.option('--diff', action='store_true', help='diff against previous report')
@cmdln.option('--dry', action='store_true', help='perform a dry-run where applicable')
@cmdln.option('--force-refresh', action='store_true', help='force refresh of data')
@cmdln.option('--format', default='plain', help='output format')
@cmdln.option('--listen', action='store_true', help='listen to events')
@cmdln.option('--listen-seconds', help='number of seconds to listen to events')
@cmdln.option('--mail', action='store_true', help='mail report to <confg:mail-release-list>')
@cmdln.option('--origins-only', action='store_true', help='list origins instead of expanded config')
@cmdln.option('-p', '--project', help='project on which to operate')
def do_origin(self, subcmd, opts, *args):
"""${cmd_name}: tools for working with origin information
${cmd_option_list}
config: print expanded OSRT:OriginConfig
cron: update the lookup for all projects with an OSRT:OriginConfig attribute
history: list requests containing an origin annotation
list: print all packages and their origin
package: print the origin of package
potentials: list potential origins of a package
projects: list all projects with an OSRT:OriginConfig attribute
report: print origin summary report
update: handle package source changes as either delete or submit requests
Usage:
osc origin config [--origins-only]
osc origin cron
osc origin history [--format json|yaml] PACKAGE
osc origin list [--force-refresh] [--format json|yaml]
osc origin package [--debug] PACKAGE
osc origin potentials [--format json|yaml] PACKAGE
osc origin projects [--format json|yaml]
osc origin report [--diff] [--force-refresh] [--mail]
osc origin update [--listen] [--listen-seconds] [PACKAGE...]
"""
if len(args) == 0:
raise oscerr.WrongArgs('A command must be indicated.')
command = args[0]
if command not in ['config', 'cron', 'history', 'list', 'package', 'potentials',
'projects', 'report', 'update']:
raise oscerr.WrongArgs('Unknown command: {}'.format(command))
if command == 'package' and len(args) < 2:
raise oscerr.WrongArgs('A package must be indicated.')
level = logging.DEBUG if opts.debug else None
if command == 'update':
# Only way to include thread in pika log message.
logging.basicConfig(level=level, format='<%(threadName)s> [%(levelname).1s] %(message)s')
else:
logging.basicConfig(level=level, format='[%(levelname).1s] %(message)s')
# Allow for determining project from osc store.
if not opts.project and core.is_project_dir('.'):
opts.project = core.store_read_project('.')
Cache.init()
apiurl = self.get_api_url()
if command not in ['cron', 'projects', 'update']:
if not opts.project:
raise oscerr.WrongArgs('A project must be indicated.')
config = config_load(apiurl, opts.project)
if not config:
raise oscerr.WrongArgs('OSRT:OriginConfig attribute missing from {}'.format(opts.project))
sentry_sdk = sentry_init(apiurl, {'osc_plugin': subcmd})
try:
function = 'osrt_origin_{}'.format(command)
globals()[function](apiurl, opts, *args[1:])
except Exception as e:
# Capture exception as osc.babysitter will consume any plugin exception.
sentry_sdk.capture_exception(e)
raise e
def osrt_origin_config(apiurl, opts, *args):
config = config_load(apiurl, opts.project)
if opts.origins_only:
print('\n'.join(config_origin_list(config)))
else:
yaml.Dumper.ignore_aliases = lambda *args: True
print(yaml.dump(config))
def osrt_origin_cron(apiurl, opts, *args):
projects = project_attribute_list(apiurl, 'OSRT:OriginConfig')
for project in projects:
# Preserve cache for locked projects, but create if missing.
if project_locked(apiurl, project):
lookup_path = osrt_origin_lookup_file(project)
if os.path.exists(lookup_path):
# Update the last accessed time to avoid cache manager culling.
os.utime(lookup_path, (time.time(), os.stat(lookup_path).st_mtime))
print('{}<locked> lookup preserved'.format(project))
continue
# Force update lookup information.
lookup = osrt_origin_lookup(apiurl, project, force_refresh=True, quiet=True)
print('{} lookup updated for {} package(s)'.format(project, len(lookup)))
def osrt_origin_dump(format, data):
if format == 'json':
print(json.dumps(data))
elif format == 'yaml':
print(yaml.dump(data))
else:
if format != 'plain':
print('unknown format: {}'.format(format), file=sys.stderr)
return False
return True
def osrt_origin_history(apiurl, opts, *packages):
config = config_load(apiurl, opts.project)
history = origin_history(apiurl, opts.project, packages[0], config['review-user'])
if osrt_origin_dump(opts.format, history):
return
line_format = '{:<50} {:<10} {:>7}'
print(line_format.format('origin', 'state', 'request'))
for record in history:
print(line_format.format(record['origin'], record['state'], record['request']))
def osrt_origin_lookup_file(project, previous=False):
parts = [project, 'yaml']
if previous:
parts.insert(1, 'previous')
lookup_name = '.'.join(parts)
cache_dir = CacheManager.directory('origin-manager')
return os.path.join(cache_dir, lookup_name)
def osrt_origin_lookup(apiurl, project, force_refresh=False, previous=False, quiet=False):
locked = project_locked(apiurl, project)
if locked:
force_refresh = False
lookup_path = osrt_origin_lookup_file(project, previous)
if not force_refresh and os.path.exists(lookup_path):
if not locked and not previous:
# Force refresh of lookup information if expried.
if time.time() - os.stat(lookup_path).st_mtime > OSRT_ORIGIN_LOOKUP_TTL:
return osrt_origin_lookup(apiurl, project, True)
with open(lookup_path, 'r') as lookup_stream:
lookup = yaml.safe_load(lookup_stream)
if not isinstance(next(iter(lookup.values())), dict):
# Convert flat format to dictionary.
for package, origin in lookup.items():
lookup[package] = {'origin': origin}
else:
if previous:
return None
packages = package_list_kind_filtered(apiurl, project)
lookup = {}
for package in packages:
origin_info = origin_find(apiurl, project, package)
lookup[str(package)] = {
'origin': str(origin_info),
'revisions': origin_revision_state(apiurl, project, package, origin_info),
}
if os.path.exists(lookup_path):
lookup_path_previous = osrt_origin_lookup_file(project, True)
copyfile(lookup_path, lookup_path_previous)
with open(lookup_path, 'w+') as lookup_stream:
yaml.dump(lookup, lookup_stream, default_flow_style=False)
if not previous and not quiet:
dt = timedelta(seconds=time.time() - os.stat(lookup_path).st_mtime)
print('# generated {} ago'.format(dt), file=sys.stderr)
return lookup
def osrt_origin_max_key(dictionary, minimum):
return max(len(max(dictionary.keys(), key=len)), minimum)
def osrt_origin_list(apiurl, opts, *args):
lookup = osrt_origin_lookup(apiurl, opts.project, opts.force_refresh, quiet=opts.format != 'plain')
if opts.format != 'plain':
# Suppliment data with request information.
requests = get_request_list(apiurl, opts.project, None, None, ['new', 'review'], 'submit')
requests.extend(get_request_list(apiurl, opts.project, None, None, ['new', 'review'], 'delete'))
requests_map = {}
for request in requests:
for action in request.actions:
requests_map[action.tgt_package] = request.reqid
# Convert data from lookup to list.
out = []
for package, details in sorted(lookup.items()):
out.append({
'package': package,
'origin': details['origin'],
'revisions': details.get('revisions', []),
'request': requests_map.get(package),
})
osrt_origin_dump(opts.format, out)
return
line_format = '{:<' + str(osrt_origin_max_key(lookup, 7)) + '} {}'
print(line_format.format('package', 'origin'))
for package, details in sorted(lookup.items()):
print(line_format.format(package, details['origin']))
def osrt_origin_package(apiurl, opts, *packages):
origin_info = origin_find(apiurl, opts.project, packages[0])
print(origin_info)
def osrt_origin_potentials(apiurl, opts, *packages):
potentials = origin_potentials(apiurl, opts.project, packages[0])
if opts.format != 'plain':
out = []
for origin, version in potentials.items():
out.append({'origin': origin, 'version': version})
osrt_origin_dump(opts.format, out)
return
line_format = '{:<50} {}'
print(line_format.format('origin', 'version'))
for origin, version in potentials.items():
print(line_format.format(origin, version))
def osrt_origin_projects(apiurl, opts, *args):
projects = list(project_attribute_list(apiurl, 'OSRT:OriginConfig'))
if osrt_origin_dump(opts.format, projects):
return
for project in sorted(projects):
print(project)
def osrt_origin_report_count(lookup):
origin_count = {}
for package, details in lookup.items():
origin_count.setdefault(details['origin'], 0)
origin_count[details['origin']] += 1
return origin_count
def osrt_origin_report_count_diff(origin_count, origin_count_previous):
origin_count_change = {}
for origin, count in origin_count.items():
delta = count - origin_count_previous.get(origin, 0)
delta = '+' + str(delta) if delta > 0 else str(delta)
origin_count_change[origin] = delta
return origin_count_change
def osrt_origin_report_diff(lookup, lookup_previous):
diff = {}
for package, details in lookup.items():
origin_previous = lookup_previous.get(package, {}).get('origin')
if details['origin'] != origin_previous:
diff[package] = (details['origin'], origin_previous)
return diff
def osrt_origin_report(apiurl, opts, *args):
lookup = osrt_origin_lookup(apiurl, opts.project, opts.force_refresh)
origin_count = osrt_origin_report_count(lookup)
columns = ['origin', 'count', 'percent']
column_formats = [
'{:<' + str(osrt_origin_max_key(origin_count, 6)) + '}',
'{:>5}',
'{:>7}',
]
if opts.diff:
columns.insert(2, 'change')
column_formats.insert(2, '{:>6}')
lookup_previous = osrt_origin_lookup(apiurl, opts.project, previous=True)
if lookup_previous is not None:
origin_count_previous = osrt_origin_report_count(lookup_previous)
origin_count_change = osrt_origin_report_count_diff(origin_count, origin_count_previous)
package_diff = osrt_origin_report_diff(lookup, lookup_previous)
else:
origin_count_change = {}
package_diff = []
line_format = ' '.join(column_formats)
report = [line_format.format(*columns)]
total = len(lookup)
for origin, count in sorted(origin_count.items(), key=lambda x: x[1], reverse=True):
values = [origin, count, round(float(count) / total * 100, 2)]
if opts.diff:
values.insert(2, origin_count_change.get(origin, 0))
report.append(line_format.format(*values))
if opts.diff and len(package_diff):
line_format = '{:<' + str(osrt_origin_max_key(package_diff, 7)) + '} ' + \
' '.join([column_formats[0]] * 2)
report.append('')
report.append(line_format.format('package', 'origin', 'origin previous'))
for package, origins in sorted(package_diff.items()):
report.append(line_format.format(package, *origins))
body = '\n'.join(report)
print(body)
if opts.mail:
mail_send(apiurl, opts.project, 'release-list', '{} origin report'.format(opts.project),
body, None, dry=opts.dry)
def osrt_origin_update(apiurl, opts, *packages):
if opts.listen:
from osclib.origin_listener import OriginSourceChangeListener
logger = logging.getLogger()
logger.setLevel(logging.INFO)
listener = OriginSourceChangeListener(apiurl, logger, opts.project, opts.dry)
try:
runtime = int(opts.listen_seconds) if opts.listen_seconds else None
listener.run(runtime=runtime)
except KeyboardInterrupt:
listener.stop()
return
if not opts.project:
for project in origin_updatable(apiurl):
opts.project = project
osrt_origin_update(apiurl, opts, *packages)
return
if len(packages) == 0:
packages = osrt_origin_update_packages(apiurl, opts.project)
for package in packages:
print('checking for updates to {}/{}...'.format(opts.project, package))
request_future = origin_update(apiurl, opts.project, package)
if request_future:
request_future.print_and_create(opts.dry)
def osrt_origin_update_packages(apiurl, project):
packages = set(package_list_kind_filtered(apiurl, project))
# Include packages from origins with initial update enabled to allow for
# potential new package submissions.
for origin in origin_updatable_initial(apiurl, project):
for package in package_list(apiurl, origin):
# Only add missing package if it does not exist in target
# project. If it exists in target then it is not a source
# package (since origin list is filtered to source) and should
# not be updated. This also properly avoids submitting a package
# that is a subpackage in target, but is a source package in an
# origin project.
if package in packages or entity_exists(apiurl, project, package):
continue
# No sense submitting a non-source package (most expensive).
if package_kind(apiurl, origin, package) == 'source':
packages.add(package)
return packages