-
Notifications
You must be signed in to change notification settings - Fork 56
/
backend_general.py
executable file
·488 lines (400 loc) · 16.1 KB
/
backend_general.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
import flask
from copr_common.enums import StatusEnum
from coprs import db, app
from coprs import models
from coprs.logic import actions_logic
from coprs.logic.builds_logic import BuildsLogic
from coprs.logic.complex_logic import ComplexLogic, BuildConfigLogic
from coprs.logic.packages_logic import PackagesLogic
from coprs.logic.coprs_logic import MockChrootsLogic, CoprChrootsLogic
from coprs.exceptions import CoprHttpException, ObjectNotFound
from coprs.helpers import streamed_json
from coprs.views import misc
from coprs.views.backend_ns import backend_ns
@backend_ns.after_request
def send_frontend_version(response):
"""
This sets the FE <=> BE API version. We should bump this version anytime we
do something new with the protocol. On the Backend/builder side we can
setup the version according to our needs.
For the backend counterpart, see the `MIN_FE_BE_API` constant.
"""
response.headers['Copr-FE-BE-API-Version'] = '6'
return response
@backend_ns.route("/importing/")
def dist_git_importing_queue():
"""
Return list of builds that are waiting for dist-git to import the sources.
"""
def _stream():
builds_for_import = BuildsLogic.get_build_importing_queue()
for build in builds_for_import:
task = get_import_record(build)
yield task
return streamed_json(_stream())
@backend_ns.route("/get-import-task/<build_id>")
def get_import_task(build_id):
"""
Return a single task that DistGit should import
"""
build = BuildsLogic.get(build_id).one_or_none()
task = get_import_record(build)
return flask.jsonify(task)
def get_import_record(build):
"""
Transform an ORM Build instance into a Python dictionary that is later
converted to a JSON string and sent (as task build instructions) to Copr
DistGit machine.
"""
if not build:
return None
branches = set()
for b_ch in build.build_chroots:
branches.add(b_ch.mock_chroot.distgit_branch_name)
return {
"build_id": build.id,
"owner": build.copr.owner_name,
"project": build.copr_dirname,
# TODO: we mix PR with normal builds here :-(
"branches": list(branches),
"pkg_name": build.package.name,
"srpm_url": build.srpm_url,
"sandbox": build.sandbox,
"background": build.is_background,
}
@backend_ns.route("/import-completed/", methods=["POST", "PUT"])
@misc.backend_authenticated
def dist_git_upload_completed():
app.logger.debug(flask.request.json)
build_id = flask.request.json.get("build_id")
try:
build = ComplexLogic.get_build_safe(build_id)
except ObjectNotFound:
return flask.jsonify({"updated": False})
collected_branch_chroots = []
for branch, git_hash in flask.request.json.get("branch_commits", {}).items():
branch_chroots = BuildsLogic.get_buildchroots_by_build_id_and_branch(build_id, branch)
for ch in branch_chroots:
collected_branch_chroots.append((ch.task_id))
if ch.status == StatusEnum("skipped"):
continue
ch.status = StatusEnum("pending")
ch.git_hash = git_hash
db.session.add(ch)
final_source_status = StatusEnum("succeeded")
for ch in build.build_chroots:
if ch.task_id not in collected_branch_chroots:
final_source_status = StatusEnum("failed")
ch.status = StatusEnum("failed")
db.session.add(ch)
build.source_status = final_source_status
db.session.add(build)
db.session.commit()
BuildsLogic.delete_local_source(build)
return flask.jsonify({"updated": True})
def get_build_record(task, for_backend=False):
"""
Transform an ORM BuildChroot instance into a Python dictionary that is later
converted to a JSON string and sent (as task build instructions) to Copr
Backend or Copr Builder machine.
The Backend needs only a limited amount of information to correctly schedule
the task processing (what to build, when, how, where...), whilst Builder
needs the full information to properly perform the build.
The build queue may be rather large (tens of thousands tasks) in some peak
situations, so we try to really limit the amount of data processed and sent
to Backend (array). OTOH, Builder's single-row queries are rather cheap and
thus we don't have to pay attention to such optimizations.
:param for_backend: True if the data are consumed by Backend (smaller
dictionary output), False if the data are consumed by Builder (full task
info).
"""
if not task:
return None
build_record = None
try:
build_record = {
"task_id": task.task_id,
"build_id": task.build.id,
"project_owner": task.build.copr.owner_name,
"sandbox": task.build.sandbox,
"background": bool(task.build.is_background),
"chroot": task.mock_chroot.name,
"tags": task.mock_chroot.tags,
}
if for_backend:
return build_record
build_record.update({
"project_name": task.build.copr_name,
"project_dirname": task.build.copr_dirname,
"submitter": task.build.submitter[0],
"repos": task.build.repos,
"memory_reqs": task.build.memory_reqs,
"timeout": task.build.timeout,
"enable_net": task.build.enable_net,
"git_repo": task.distgit_clone_url,
"git_hash": task.git_hash,
"package_name": task.build.package.name,
"package_version": task.build.pkg_version,
"uses_devel_repo": task.build.copr.devel_mode,
"isolation": task.build.isolation,
"fedora_review": task.build.copr.fedora_review,
"appstream": bool(task.build.appstream),
})
copr_chroot = CoprChrootsLogic.get_by_name_safe(task.build.copr, task.mock_chroot.name)
modules = copr_chroot.module_setup_commands
if modules:
build_record["modules"] = {'toggle': modules}
build_config = BuildConfigLogic.generate_build_config(task.build.copr, task.mock_chroot.name)
build_record["repos"] = build_config.get("repos")
build_record["buildroot_pkgs"] = build_config.get("additional_packages")
build_record["with_opts"] = build_config.get("with_opts")
build_record["without_opts"] = build_config.get("without_opts")
bch_bootstrap = BuildConfigLogic.build_bootstrap_setup(
build_config, task.build)
build_record.update(bch_bootstrap)
bch_isolation = BuildConfigLogic.get_build_isolation(
build_config, task.build)
build_record.update(bch_isolation)
except Exception as err:
app.logger.exception(err)
return None
return build_record
def get_srpm_build_record(task, for_backend=False):
"""
Transform an ORM Build instance (how to build SRPM) into a Python dictionary
that is later converted to a JSON string and sent (as task build
instructions) to Copr Backend or Copr Builder machine. For more info see
get_build_record() documentation.
"""
if not task:
return None
if task.source_type_text == "custom":
chroot = task.source_json_dict['chroot']
else:
chroot = None
try:
build_record = {
"task_id": task.task_id,
"build_id": task.id,
"project_owner": task.copr.owner_name,
"sandbox": task.sandbox,
"background": bool(task.is_background),
"chroot": chroot,
}
if for_backend:
return build_record
repos = (task.source_json_dict.get("repos", "") or "").split()
build_record.update({
"source_type": task.source_type,
"source_json": task.source_json,
"submitter": task.submitter[0],
"project_name": task.copr_name,
"project_dirname": task.copr_dirname,
"package_name": task.package.name if task.package else None,
"appstream": bool(task.copr.appstream),
"repos": BuildConfigLogic.get_additional_repo_views(repos, chroot),
})
except Exception as err:
app.logger.exception(err)
return None
return build_record
@backend_ns.route("/pending-action/")
def pending_action():
"""
Return a single action.
"""
action_record = None
action = actions_logic.ActionsLogic.get_waiting().first()
if action:
action_record = action.to_dict(options={
"__columns_except__": ["result", "message", "ended_on"]
})
return flask.jsonify(action_record)
@backend_ns.route("/build-tasks/cancel-requests/")
def pending_cancel_builds():
"""
Return the list of task IDs to be canceled.
"""
task_ids = [x.what for x in models.CancelRequest.query.all()]
return flask.jsonify(task_ids)
@backend_ns.route("/build-tasks/canceled/<task_id>/", methods=["POST", "PUT"])
@misc.backend_authenticated
def build_task_canceled(task_id):
""" Report back to frontend that the task was canceled on backend """
models.CancelRequest.query.filter_by(what=task_id).delete()
was_running = flask.request.json
if not was_running:
if '-' in task_id:
try:
build_chroot = BuildsLogic.get_build_task(task_id)
build_chroot.status = StatusEnum("canceled")
except ObjectNotFound:
pass
else:
build = models.Build.query.filter_by(id=task_id).first()
if build:
build.source_status = StatusEnum("canceled")
db.session.commit()
return flask.jsonify("success")
@backend_ns.route("/pending-actions/")
def pending_actions():
'get the list of actions backand should take care of'
data = []
for action in actions_logic.ActionsLogic.get_waiting():
data.append({
'id': action.id,
'priority': action.priority or action.default_priority,
})
return flask.json.dumps(data)
@backend_ns.route("/action/<int:action_id>/")
def get_action(action_id):
action = actions_logic.ActionsLogic.get(action_id).one()
action_record = action.to_dict()
return flask.jsonify(action_record)
@backend_ns.route("/pending-action-count/")
def pending_action_count():
"""
Return pending action count.
"""
return flask.jsonify(actions_logic.ActionsLogic.get_waiting().count())
@backend_ns.route("/pending-jobs/")
def pending_jobs():
"""
Return the job queue.
"""
# This code is really expensive, and takes a long time when there is a large
# build queue. We want to avoid repeated reload of models.Batch data, and
# for that we need to have it strongly referenced.
cache = set()
def build_ready(build):
""" Is the build blocked? """
cache.add(build.batch)
return not build.blocked
def _stream():
args = {"data_type": "for_backend"}
app.logger.info("Generating SRPM builds")
for build in BuildsLogic.get_pending_srpm_build_tasks(**args):
if not build_ready(build):
continue
record = get_srpm_build_record(build, for_backend=True)
yield record
app.logger.info("Generating RPM builds")
for build_chroot in BuildsLogic.get_pending_build_tasks(**args):
if not build_ready(build_chroot.build):
continue
record = get_build_record(build_chroot, for_backend=True)
yield record
return streamed_json(_stream())
@backend_ns.route("/get-build-task/<task_id>/")
@backend_ns.route("/get-build-task/<task_id>")
def get_build_task(task_id):
try:
task = BuildsLogic.get_build_task(task_id)
build_record = get_build_record(task)
return flask.jsonify(build_record)
except CoprHttpException as ex:
jsonout = flask.jsonify({"msg": str(ex)})
jsonout.status_code = ex.code
return jsonout
@backend_ns.route("/get-srpm-build-task/<build_id>/")
@backend_ns.route("/get-srpm-build-task/<build_id>")
def get_srpm_build_task(build_id):
task = BuildsLogic.get_srpm_build_task(build_id)
if not task:
jsonout = flask.jsonify({'msg': 'Specified task ID not found'})
jsonout.status_code = 404
return jsonout
build_record = get_srpm_build_record(task)
return flask.jsonify(build_record)
@backend_ns.route("/update/", methods=["POST", "PUT"])
@misc.backend_authenticated
def update():
result = {}
request_data = flask.request.json
for typ, logic_cls in [("actions", actions_logic.ActionsLogic),
("builds", BuildsLogic)]:
if typ not in request_data:
continue
to_update = {}
for obj in request_data[typ]:
to_update[obj["id"]] = obj
existing = {}
for obj in logic_cls.get_by_ids(to_update.keys()).all():
existing[obj.id] = obj
non_existing_ids = list(set(to_update.keys()) - set(existing.keys()))
for i, obj in existing.items():
logic_cls.update_state_from_dict(obj, to_update[i])
db.session.commit()
result.update({"updated_{0}_ids".format(typ): list(existing.keys()),
"non_existing_{0}_ids".format(typ): non_existing_ids})
return flask.jsonify(result)
@backend_ns.route("/starting_build/", methods=["POST", "PUT"])
@misc.backend_authenticated
def starting_build():
"""
Check if the build is not cancelled and set it to starting state
"""
data = flask.request.json
try:
build = ComplexLogic.get_build_safe(data.get('build_id'))
except ObjectNotFound:
return flask.jsonify({"can_start": False})
if build.canceled:
return flask.jsonify({"can_start": False})
BuildsLogic.update_state_from_dict(build, data)
db.session.commit()
return flask.jsonify({"can_start": True})
@backend_ns.route("/reschedule_build_chroot/", methods=["POST", "PUT"])
@misc.backend_authenticated
def reschedule_build_chroot():
response = {}
build_id = flask.request.json.get("build_id")
task_id = flask.request.json.get("task_id")
chroot = flask.request.json.get("chroot")
try:
build = ComplexLogic.get_build_safe(build_id)
except ObjectNotFound:
response["result"] = "noop"
response["msg"] = "Build {} wasn't found".format(build_id)
return flask.jsonify(response)
if build.canceled:
response["result"] = "noop"
response["msg"] = "build was cancelled, ignoring"
return flask.jsonify(response)
run_statuses = set([StatusEnum("starting"), StatusEnum("running")])
if task_id == build.task_id:
if build.source_status in run_statuses:
app.logger.info("rescheduling source build %s", build.id)
BuildsLogic.update_state_from_dict(build, {
"task_id": task_id,
"status": StatusEnum("pending")
})
db.session.commit()
response["result"] = "done"
else:
response["result"] = "noop"
response["msg"] = "build is not in running states, ignoring"
else:
build_chroot = build.chroots_dict_by_name.get(chroot)
if build_chroot and build_chroot.status in run_statuses:
app.logger.info("rescheduling build {} chroot: {}"
.format(build.id, build_chroot.name))
BuildsLogic.update_state_from_dict(build, {
"task_id": task_id,
"chroot": chroot,
"status": StatusEnum("pending")
})
db.session.commit()
response["result"] = "done"
else:
response["result"] = "noop"
response["msg"] = "build chroot is not in running states, ignoring"
return flask.jsonify(response)
@backend_ns.route("/chroots-prunerepo-status/")
def chroots_prunerepo_status():
return flask.jsonify(MockChrootsLogic.chroots_prunerepo_status())
@backend_ns.route("/final-prunerepo-done/", methods=["POST", "PUT"])
@misc.backend_authenticated
def final_prunerepo_done():
chroots_pruned = flask.request.get_json()
return flask.jsonify(MockChrootsLogic.prunerepo_finished(chroots_pruned))