OLD | NEW |
1 # Copyright 2016 The Chromium Authors. All rights reserved. | 1 # Copyright 2016 The Chromium Authors. All rights reserved. |
2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 import logging | 5 import logging |
6 import datetime | 6 import datetime |
7 import math | 7 import math |
8 import os | 8 import os |
9 import sys | 9 import sys |
10 import time | 10 import time |
(...skipping 20 matching lines...) Expand all Loading... |
31 clovis_logger = logging.getLogger('clovis_frontend') | 31 clovis_logger = logging.getLogger('clovis_frontend') |
32 clovis_logger.setLevel(logging.DEBUG) | 32 clovis_logger.setLevel(logging.DEBUG) |
33 project_name = app_identity.get_application_id() | 33 project_name = app_identity.get_application_id() |
34 instance_helper = common.google_instance_helper.GoogleInstanceHelper( | 34 instance_helper = common.google_instance_helper.GoogleInstanceHelper( |
35 credentials=GoogleCredentials.get_application_default(), | 35 credentials=GoogleCredentials.get_application_default(), |
36 project=project_name, | 36 project=project_name, |
37 logger=clovis_logger) | 37 logger=clovis_logger) |
38 app = flask.Flask(__name__) | 38 app = flask.Flask(__name__) |
39 | 39 |
40 | 40 |
41 def Render(message, memory_logs=None): | 41 def RenderJobCreationPage(message, memory_logs=None): |
42 """Renders the log.html template. | 42 """Renders the log.html template. |
43 | 43 |
44 Args: | 44 Args: |
45 message (str): Main content of the page. | 45 message (str): Main content of the page. |
46 memory_logs (MemoryLogs): Optional logs. | 46 memory_logs (MemoryLogs): Optional logs. |
47 """ | 47 """ |
48 log = None | 48 log = None |
49 if memory_logs: | 49 if memory_logs: |
50 log = memory_logs.Flush().split('\n') | 50 log = memory_logs.Flush().split('\n') |
51 return flask.render_template('log.html', body=message, log=log) | 51 return flask.render_template('log.html', body=message, log=log, |
| 52 title='Job Creation Status') |
52 | 53 |
53 | 54 |
54 def PollWorkers(tag, start_time, timeout_hours, email_address, task_url): | 55 def PollWorkers(tag, start_time, timeout_hours, email_address, task_url): |
55 """Checks if there are workers associated with tag, by polling the instance | 56 """Checks if there are workers associated with tag, by polling the instance |
56 group. When all workers are finished, the instance group and the instance | 57 group. When all workers are finished, the instance group and the instance |
57 template are destroyed. | 58 template are destroyed. |
58 After some timeout delay, the instance group is destroyed even if there are | 59 After some timeout delay, the instance group is destroyed even if there are |
59 still workers associated to it, which has the effect of killing all these | 60 still workers associated to it, which has the effect of killing all these |
60 workers. | 61 workers. |
61 | 62 |
(...skipping 254 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
316 def StartFromJsonString(http_body_str): | 317 def StartFromJsonString(http_body_str): |
317 """Main function handling a JSON task posted by the user.""" | 318 """Main function handling a JSON task posted by the user.""" |
318 # Set up logging. | 319 # Set up logging. |
319 memory_logs = MemoryLogs(clovis_logger) | 320 memory_logs = MemoryLogs(clovis_logger) |
320 memory_logs.Start() | 321 memory_logs.Start() |
321 | 322 |
322 # Load the task from JSON. | 323 # Load the task from JSON. |
323 task = ClovisTask.FromJsonString(http_body_str) | 324 task = ClovisTask.FromJsonString(http_body_str) |
324 if not task: | 325 if not task: |
325 clovis_logger.error('Invalid JSON task.') | 326 clovis_logger.error('Invalid JSON task.') |
326 return Render('Invalid JSON task:\n' + http_body_str, memory_logs) | 327 return RenderJobCreationPage( |
| 328 'Invalid JSON task:\n' + http_body_str, memory_logs) |
327 | 329 |
328 task_tag = task.BackendParams()['tag'] | 330 task_tag = task.BackendParams()['tag'] |
329 clovis_logger.info('Start processing %s task with tag %s.' % (task.Action(), | 331 clovis_logger.info('Start processing %s task with tag %s.' % (task.Action(), |
330 task_tag)) | 332 task_tag)) |
331 user_email = email_helper.GetUserEmail() | 333 user_email = email_helper.GetUserEmail() |
332 | 334 |
333 # Write the job to the datastore. | 335 # Write the job to the datastore. |
334 frontend_job = FrontendJob.CreateForTag(task_tag) | 336 frontend_job = FrontendJob.CreateForTag(task_tag) |
335 frontend_job.email = user_email | 337 frontend_job.email = user_email |
336 frontend_job.status = 'not_started' | 338 frontend_job.status = 'not_started' |
337 frontend_job.clovis_task = task.ToJsonString() | 339 frontend_job.clovis_task = task.ToJsonString() |
338 frontend_job.put() | 340 frontend_job.put() |
339 | 341 |
340 # Process the job on the queue, to avoid timeout issues. | 342 # Process the job on the queue, to avoid timeout issues. |
341 deferred.defer(SpawnTasksOnBackgroundQueue, task_tag) | 343 deferred.defer(SpawnTasksOnBackgroundQueue, task_tag) |
342 | 344 |
343 return Render( | 345 return RenderJobCreationPage( |
344 flask.Markup( | 346 flask.Markup( |
345 '<a href="%s">See progress.</a>' % FrontendJob.GetJobURL(task_tag)), | 347 '<a href="%s">See progress.</a>' % FrontendJob.GetJobURL(task_tag)), |
346 memory_logs) | 348 memory_logs) |
347 | 349 |
348 | 350 |
349 def SpawnTasksOnBackgroundQueue(task_tag): | 351 def SpawnTasksOnBackgroundQueue(task_tag): |
350 """Spawns Clovis tasks associated with task_tag from the backgound queue. | 352 """Spawns Clovis tasks associated with task_tag from the backgound queue. |
351 | 353 |
352 This function is mostly a wrapper around SpawnTasks() that catches exceptions. | 354 This function is mostly a wrapper around SpawnTasks() that catches exceptions. |
353 It is assumed that a FrontendJob for task_tag exists. | 355 It is assumed that a FrontendJob for task_tag exists. |
(...skipping 75 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
429 sequential_duration_s / target_parallel_duration_s) | 431 sequential_duration_s / target_parallel_duration_s) |
430 | 432 |
431 # Check the instance quotas. | 433 # Check the instance quotas. |
432 clovis_logger.info( | 434 clovis_logger.info( |
433 'Requesting %i instances.' % task.BackendParams()['instance_count']) | 435 'Requesting %i instances.' % task.BackendParams()['instance_count']) |
434 frontend_job.status = 'checking_instance_quotas' | 436 frontend_job.status = 'checking_instance_quotas' |
435 max_instances = instance_helper.GetAvailableInstanceCount() | 437 max_instances = instance_helper.GetAvailableInstanceCount() |
436 if max_instances == -1: | 438 if max_instances == -1: |
437 frontend_job.status = 'instance_count_error' | 439 frontend_job.status = 'instance_count_error' |
438 return | 440 return |
439 elif task.BackendParams()['instance_count'] == 0: | 441 elif max_instances == 0 and task.BackendParams()['instance_count'] > 0: |
440 frontend_job.status = 'no_instance_available_error' | 442 frontend_job.status = 'no_instance_available_error' |
441 return | 443 return |
442 elif max_instances < task.BackendParams()['instance_count']: | 444 elif max_instances < task.BackendParams()['instance_count']: |
443 clovis_logger.warning( | 445 clovis_logger.warning( |
444 'Instance count limited by quota: %i available / %i requested.' % ( | 446 'Instance count limited by quota: %i available / %i requested.' % ( |
445 max_instances, task.BackendParams()['instance_count'])) | 447 max_instances, task.BackendParams()['instance_count'])) |
446 task.BackendParams()['instance_count'] = max_instances | 448 task.BackendParams()['instance_count'] = max_instances |
447 | 449 |
448 # Compute the timeout if there is none specified. | 450 # Compute the timeout if there is none specified. |
449 expected_duration_s = sequential_duration_s / ( | 451 expected_duration_s = sequential_duration_s / ( |
(...skipping 64 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
514 def Root(): | 516 def Root(): |
515 """Home page: show the new task form.""" | 517 """Home page: show the new task form.""" |
516 return flask.render_template('form.html') | 518 return flask.render_template('form.html') |
517 | 519 |
518 | 520 |
519 @app.route('/form_sent', methods=['POST']) | 521 @app.route('/form_sent', methods=['POST']) |
520 def StartFromForm(): | 522 def StartFromForm(): |
521 """HTML form endpoint.""" | 523 """HTML form endpoint.""" |
522 data_stream = flask.request.files.get('json_task') | 524 data_stream = flask.request.files.get('json_task') |
523 if not data_stream: | 525 if not data_stream: |
524 return Render('Failed, no content.') | 526 return RenderJobCreationPage('Failed, no content.') |
525 http_body_str = data_stream.read() | 527 http_body_str = data_stream.read() |
526 return StartFromJsonString(http_body_str) | 528 return StartFromJsonString(http_body_str) |
527 | 529 |
528 | 530 |
| 531 @app.route('/kill_job') |
| 532 def KillJob(): |
| 533 tag = flask.request.args.get('tag') |
| 534 page_title = 'Kill Job' |
| 535 if not tag: |
| 536 return flask.render_template('log.html', body='Failed: Invalid tag.', |
| 537 title=page_title) |
| 538 |
| 539 frontend_job = FrontendJob.GetFromTag(tag) |
| 540 |
| 541 if not frontend_job: |
| 542 return flask.render_template('log.html', body='Job not found.', |
| 543 title=page_title) |
| 544 Finalize(tag, frontend_job.email, 'CANCELED', frontend_job.task_url) |
| 545 |
| 546 body = 'Killed job %s.' % tag |
| 547 return flask.render_template('log.html', body=body, title=page_title) |
| 548 |
| 549 |
529 @app.route('/list_jobs') | 550 @app.route('/list_jobs') |
530 def ShowTaskList(): | 551 def ShowJobList(): |
531 """Shows a list of all active jobs.""" | 552 """Shows a list of all active jobs.""" |
532 tags = FrontendJob.ListJobs() | 553 tags = FrontendJob.ListJobs() |
| 554 page_title = 'Active Jobs' |
533 | 555 |
534 if not tags: | 556 if not tags: |
535 Render('No active jobs.') | 557 return flask.render_template('log.html', body='No active job.', |
| 558 title=page_title) |
536 | 559 |
537 html = flask.Markup('Active tasks:<ul>') | 560 html = '' |
538 for tag in tags: | 561 for tag in tags: |
539 html += flask.Markup( | 562 html += flask.Markup( |
540 '<li><a href="%s">%s</a></li>') % (FrontendJob.GetJobURL(tag), tag) | 563 '<li><a href="%s">%s</a></li>') % (FrontendJob.GetJobURL(tag), tag) |
541 html += flask.Markup('</ul>') | 564 html += flask.Markup('</ul>') |
542 return Render(html) | 565 return flask.render_template('log.html', body=html, title=page_title) |
543 | 566 |
544 | 567 |
545 @app.route(FrontendJob.SHOW_JOB_URL) | 568 @app.route(FrontendJob.SHOW_JOB_URL) |
546 def ShowTask(): | 569 def ShowJob(): |
547 """Shows basic information abour a job.""" | 570 """Shows basic information abour a job.""" |
548 tag = flask.request.args.get('tag') | 571 tag = flask.request.args.get('tag') |
| 572 page_title = 'Job Information' |
549 if not tag: | 573 if not tag: |
550 return Render('Invalid task tag.') | 574 return flask.render_template('log.html', body='Invalid tag.', |
| 575 title=page_title) |
551 | 576 |
552 frontend_job = FrontendJob.GetFromTag(tag) | 577 frontend_job = FrontendJob.GetFromTag(tag) |
553 | 578 |
554 if not frontend_job: | 579 if not frontend_job: |
555 return Render('Task not found.') | 580 return flask.render_template('log.html', body='Job not found.', |
556 | 581 title=page_title) |
557 message = flask.Markup('Task details:' + frontend_job.RenderAsHtml()) | |
558 | 582 |
559 log = None | 583 log = None |
560 if frontend_job.log: | 584 if frontend_job.log: |
561 log = frontend_job.log.split('\n') | 585 log = frontend_job.log.split('\n') |
562 | 586 |
563 return flask.render_template('log.html', body=message, log=log) | 587 body = flask.Markup(frontend_job.RenderAsHtml()) |
| 588 body += flask.Markup('<a href="/kill_job?tag=%s">Kill</a>' % tag) |
| 589 return flask.render_template('log.html', log=log, title=page_title, |
| 590 body=body) |
564 | 591 |
565 | 592 |
566 @app.errorhandler(404) | 593 @app.errorhandler(404) |
567 def PageNotFound(e): # pylint: disable=unused-argument | 594 def PageNotFound(e): # pylint: disable=unused-argument |
568 """Return a custom 404 error.""" | 595 """Return a custom 404 error.""" |
569 return 'Sorry, Nothing at this URL.', 404 | 596 return 'Sorry, Nothing at this URL.', 404 |
570 | 597 |
571 | 598 |
572 @app.errorhandler(500) | 599 @app.errorhandler(500) |
573 def ApplicationError(e): | 600 def ApplicationError(e): |
574 """Return a custom 500 error.""" | 601 """Return a custom 500 error.""" |
575 return 'Sorry, unexpected error: {}'.format(e), 499 | 602 return 'Sorry, unexpected error: {}'.format(e), 499 |
OLD | NEW |