mirror of
git://git.yoctoproject.org/layerindex-web.git
synced 2025-07-19 20:59:01 +02:00
Record and display update logs
At the moment it's a bit difficult to get update logs out of the environment in which the update script is being run. In order to make the logs more accessible, create a LayerUpdate model to record the output of update_layer.py separately for each layerbranch and tie the created LayerUpdates together with a single Update model per session. We provide two ways to look at this - a Tools->Updates page for logged-in users, and there's also an "Updates" tab on each layer that is accessible to anyone; which one is useful depends on whether you are looking at the index as a whole or an individual layer. Update records older than 30 days are deleted automatically by default. Signed-off-by: Paul Eggleton <paul.eggleton@linux.intel.com>
This commit is contained in:
parent
cc1d82f893
commit
43203c578c
2
TODO
2
TODO
|
@ -35,10 +35,8 @@ Other
|
||||||
* Create simple script to check for unlisted layer subdirectories in all repos
|
* Create simple script to check for unlisted layer subdirectories in all repos
|
||||||
* Auto-detect more values from github pages?
|
* Auto-detect more values from github pages?
|
||||||
* Ability for submitters to get email notification about publication?
|
* Ability for submitters to get email notification about publication?
|
||||||
* Update script still seems not to be always printing layer name on parsing warnings/errors
|
|
||||||
* Update script could send warnings when parsing layers to maintainers? (optional)
|
* Update script could send warnings when parsing layers to maintainers? (optional)
|
||||||
* Click on OE-Classic graph element to go to query?
|
* Click on OE-Classic graph element to go to query?
|
||||||
* Use bar instead of pie graphs for OE-Classic statistics
|
* Use bar instead of pie graphs for OE-Classic statistics
|
||||||
* Ensure OE-Core appears before meta-oe in layer list
|
* Ensure OE-Core appears before meta-oe in layer list
|
||||||
* Ability for reviewers to comment before publishing a layer?
|
* Ability for reviewers to comment before publishing a layer?
|
||||||
* Record update & parse errors against recipe/layer
|
|
||||||
|
|
|
@ -74,6 +74,12 @@ class LayerDependencyAdmin(CompareVersionAdmin):
|
||||||
class LayerNoteAdmin(CompareVersionAdmin):
|
class LayerNoteAdmin(CompareVersionAdmin):
|
||||||
list_filter = ['layer__name']
|
list_filter = ['layer__name']
|
||||||
|
|
||||||
|
class UpdateAdmin(admin.ModelAdmin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class LayerUpdateAdmin(admin.ModelAdmin):
|
||||||
|
list_filter = ['update__started', 'layerbranch__layer__name', 'layerbranch__branch__name']
|
||||||
|
|
||||||
class RecipeAdmin(admin.ModelAdmin):
|
class RecipeAdmin(admin.ModelAdmin):
|
||||||
search_fields = ['filename', 'pn']
|
search_fields = ['filename', 'pn']
|
||||||
list_filter = ['layerbranch__layer__name', 'layerbranch__branch__name']
|
list_filter = ['layerbranch__layer__name', 'layerbranch__branch__name']
|
||||||
|
@ -144,6 +150,8 @@ admin.site.register(LayerBranch, LayerBranchAdmin)
|
||||||
admin.site.register(LayerMaintainer, LayerMaintainerAdmin)
|
admin.site.register(LayerMaintainer, LayerMaintainerAdmin)
|
||||||
admin.site.register(LayerDependency, LayerDependencyAdmin)
|
admin.site.register(LayerDependency, LayerDependencyAdmin)
|
||||||
admin.site.register(LayerNote, LayerNoteAdmin)
|
admin.site.register(LayerNote, LayerNoteAdmin)
|
||||||
|
admin.site.register(Update, UpdateAdmin)
|
||||||
|
admin.site.register(LayerUpdate, LayerUpdateAdmin)
|
||||||
admin.site.register(Recipe, RecipeAdmin)
|
admin.site.register(Recipe, RecipeAdmin)
|
||||||
admin.site.register(RecipeFileDependency)
|
admin.site.register(RecipeFileDependency)
|
||||||
admin.site.register(Machine, MachineAdmin)
|
admin.site.register(Machine, MachineAdmin)
|
||||||
|
|
46
layerindex/migrations/0005_layerupdate.py
Normal file
46
layerindex/migrations/0005_layerupdate.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('layerindex', '0004_layerdependency_required'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='LayerUpdate',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('started', models.DateTimeField()),
|
||||||
|
('finished', models.DateTimeField()),
|
||||||
|
('errors', models.IntegerField(default=0)),
|
||||||
|
('warnings', models.IntegerField(default=0)),
|
||||||
|
('log', models.TextField(blank=True)),
|
||||||
|
('layerbranch', models.ForeignKey(to='layerindex.LayerBranch')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Update',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('started', models.DateTimeField()),
|
||||||
|
('finished', models.DateTimeField(null=True, blank=True)),
|
||||||
|
('log', models.TextField(blank=True)),
|
||||||
|
('reload', models.BooleanField(help_text='Was this update a reload?', verbose_name='Reloaded', default=False)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='branch',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(max_length=50, verbose_name='Branch name'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='layerupdate',
|
||||||
|
name='update',
|
||||||
|
field=models.ForeignKey(to='layerindex.Update'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -31,7 +31,7 @@ class PythonEnvironment(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class Branch(models.Model):
|
class Branch(models.Model):
|
||||||
name = models.CharField(max_length=50)
|
name = models.CharField('Branch name', max_length=50)
|
||||||
bitbake_branch = models.CharField(max_length=50)
|
bitbake_branch = models.CharField(max_length=50)
|
||||||
short_description = models.CharField(max_length=50, blank=True)
|
short_description = models.CharField(max_length=50, blank=True)
|
||||||
sort_priority = models.IntegerField(blank=True, null=True)
|
sort_priority = models.IntegerField(blank=True, null=True)
|
||||||
|
@ -47,6 +47,16 @@ class Branch(models.Model):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class Update(models.Model):
|
||||||
|
started = models.DateTimeField()
|
||||||
|
finished = models.DateTimeField(blank=True, null=True)
|
||||||
|
log = models.TextField(blank=True)
|
||||||
|
reload = models.BooleanField('Reloaded', default=False, help_text='Was this update a reload?')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return '%s' % self.started
|
||||||
|
|
||||||
|
|
||||||
class LayerItem(models.Model):
|
class LayerItem(models.Model):
|
||||||
LAYER_STATUS_CHOICES = (
|
LAYER_STATUS_CHOICES = (
|
||||||
('N', 'New'),
|
('N', 'New'),
|
||||||
|
@ -255,6 +265,31 @@ class LayerNote(models.Model):
|
||||||
return "%s: %s" % (self.layer.name, self.text)
|
return "%s: %s" % (self.layer.name, self.text)
|
||||||
|
|
||||||
|
|
||||||
|
class LayerUpdate(models.Model):
|
||||||
|
layerbranch = models.ForeignKey(LayerBranch)
|
||||||
|
update = models.ForeignKey(Update)
|
||||||
|
started = models.DateTimeField()
|
||||||
|
finished = models.DateTimeField()
|
||||||
|
errors = models.IntegerField(default=0)
|
||||||
|
warnings = models.IntegerField(default=0)
|
||||||
|
log = models.TextField(blank=True)
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
warnings = 0
|
||||||
|
errors = 0
|
||||||
|
for line in self.log.splitlines():
|
||||||
|
if line.startswith('WARNING:'):
|
||||||
|
warnings += 1
|
||||||
|
elif line.startswith('ERROR:'):
|
||||||
|
errors += 1
|
||||||
|
self.warnings = warnings
|
||||||
|
self.errors = errors
|
||||||
|
super(LayerUpdate, self).save()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%s: %s: %s" % (self.layerbranch.layer.name, self.layerbranch.branch.name, self.started)
|
||||||
|
|
||||||
|
|
||||||
class Recipe(models.Model):
|
class Recipe(models.Model):
|
||||||
layerbranch = models.ForeignKey(LayerBranch)
|
layerbranch = models.ForeignKey(LayerBranch)
|
||||||
filename = models.CharField(max_length=255)
|
filename = models.CharField(max_length=255)
|
||||||
|
|
|
@ -14,6 +14,7 @@ import optparse
|
||||||
import logging
|
import logging
|
||||||
import subprocess
|
import subprocess
|
||||||
import signal
|
import signal
|
||||||
|
from datetime import datetime, timedelta
|
||||||
from distutils.version import LooseVersion
|
from distutils.version import LooseVersion
|
||||||
import utils
|
import utils
|
||||||
from layerconfparse import LayerConfParse
|
from layerconfparse import LayerConfParse
|
||||||
|
@ -41,10 +42,24 @@ def run_command_interruptible(cmd):
|
||||||
"""
|
"""
|
||||||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||||
try:
|
try:
|
||||||
ret = subprocess.call(cmd, cwd=os.path.dirname(sys.argv[0]), shell=True, preexec_fn=reenable_sigint)
|
process = subprocess.Popen(
|
||||||
|
cmd, cwd=os.path.dirname(sys.argv[0]), shell=True, preexec_fn=reenable_sigint, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
|
||||||
|
)
|
||||||
|
|
||||||
|
buf = ''
|
||||||
|
while True:
|
||||||
|
out = process.stdout.read(1)
|
||||||
|
out = out.decode('utf-8')
|
||||||
|
if out:
|
||||||
|
sys.stdout.write(out)
|
||||||
|
sys.stdout.flush()
|
||||||
|
buf += out
|
||||||
|
elif out == '' and process.poll() != None:
|
||||||
|
break
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||||
return ret
|
return process.returncode, buf
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
@ -93,7 +108,7 @@ def main():
|
||||||
|
|
||||||
utils.setup_django()
|
utils.setup_django()
|
||||||
import settings
|
import settings
|
||||||
from layerindex.models import Branch, LayerItem, LayerDependency
|
from layerindex.models import Branch, LayerItem, Update, LayerUpdate
|
||||||
|
|
||||||
logger.setLevel(options.loglevel)
|
logger.setLevel(options.loglevel)
|
||||||
|
|
||||||
|
@ -126,8 +141,21 @@ def main():
|
||||||
if not os.path.exists(fetchdir):
|
if not os.path.exists(fetchdir):
|
||||||
os.makedirs(fetchdir)
|
os.makedirs(fetchdir)
|
||||||
fetchedrepos = []
|
fetchedrepos = []
|
||||||
failedrepos = []
|
failedrepos = {}
|
||||||
|
|
||||||
|
listhandler = utils.ListHandler()
|
||||||
|
listhandler.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
|
||||||
|
logger.addHandler(listhandler)
|
||||||
|
|
||||||
|
update = Update()
|
||||||
|
update.started = datetime.now()
|
||||||
|
if options.fullreload or options.reload:
|
||||||
|
update.reload = True
|
||||||
|
else:
|
||||||
|
update.reload = False
|
||||||
|
if not options.dryrun:
|
||||||
|
update.save()
|
||||||
|
try:
|
||||||
lockfn = os.path.join(fetchdir, "layerindex.lock")
|
lockfn = os.path.join(fetchdir, "layerindex.lock")
|
||||||
lockfile = utils.lock_file(lockfn)
|
lockfile = utils.lock_file(lockfn)
|
||||||
if not lockfile:
|
if not lockfile:
|
||||||
|
@ -147,12 +175,12 @@ def main():
|
||||||
out = None
|
out = None
|
||||||
try:
|
try:
|
||||||
if not os.path.exists(repodir):
|
if not os.path.exists(repodir):
|
||||||
out = utils.runcmd("git clone %s %s" % (layer.vcs_url, urldir), fetchdir, logger=logger)
|
out = utils.runcmd("git clone %s %s" % (layer.vcs_url, urldir), fetchdir, logger=logger, printerr=False)
|
||||||
else:
|
else:
|
||||||
out = utils.runcmd("git fetch", repodir, logger=logger)
|
out = utils.runcmd("git fetch", repodir, logger=logger, printerr=False)
|
||||||
except Exception as e:
|
except subprocess.CalledProcessError as e:
|
||||||
logger.error("Fetch of layer %s failed: %s" % (layer.name, str(e)))
|
logger.error("Fetch of layer %s failed: %s" % (layer.name, e.output))
|
||||||
failedrepos.append(layer.vcs_url)
|
failedrepos[layer.vcs_url] = e.output
|
||||||
continue
|
continue
|
||||||
fetchedrepos.append(layer.vcs_url)
|
fetchedrepos.append(layer.vcs_url)
|
||||||
|
|
||||||
|
@ -173,8 +201,20 @@ def main():
|
||||||
last_rev = {}
|
last_rev = {}
|
||||||
for branch in branches:
|
for branch in branches:
|
||||||
for layer in layerquery:
|
for layer in layerquery:
|
||||||
if layer.vcs_url in failedrepos:
|
layerupdate = LayerUpdate()
|
||||||
logger.info("Skipping update of layer %s as fetch of repository %s failed" % (layer.name, layer.vcs_url))
|
layerupdate.update = update
|
||||||
|
|
||||||
|
errmsg = failedrepos.get(layer.vcs_url, '')
|
||||||
|
if errmsg:
|
||||||
|
logger.info("Skipping update of layer %s as fetch of repository %s failed:\n%s" % (layer.name, layer.vcs_url, errmsg))
|
||||||
|
layerbranch = layer.get_layerbranch(branch)
|
||||||
|
if layerbranch:
|
||||||
|
layerupdate.layerbranch = layerbranch
|
||||||
|
layerupdate.started = datetime.now()
|
||||||
|
layerupdate.finished = datetime.now()
|
||||||
|
layerupdate.log = 'ERROR: fetch failed: %s' % errmsg
|
||||||
|
if not options.dryrun:
|
||||||
|
layerupdate.save()
|
||||||
continue
|
continue
|
||||||
|
|
||||||
urldir = layer.get_fetch_dir()
|
urldir = layer.get_fetch_dir()
|
||||||
|
@ -199,8 +239,11 @@ def main():
|
||||||
cmd += ' -d'
|
cmd += ' -d'
|
||||||
elif options.loglevel == logging.ERROR:
|
elif options.loglevel == logging.ERROR:
|
||||||
cmd += ' -q'
|
cmd += ' -q'
|
||||||
|
|
||||||
logger.debug('Running layer update command: %s' % cmd)
|
logger.debug('Running layer update command: %s' % cmd)
|
||||||
ret = run_command_interruptible(cmd)
|
layerupdate.started = datetime.now()
|
||||||
|
ret, output = run_command_interruptible(cmd)
|
||||||
|
layerupdate.finished = datetime.now()
|
||||||
|
|
||||||
# We need to get layerbranch here because it might not have existed until
|
# We need to get layerbranch here because it might not have existed until
|
||||||
# layer_update.py created it, but it still may not create one (e.g. if subdir
|
# layer_update.py created it, but it still may not create one (e.g. if subdir
|
||||||
|
@ -208,6 +251,10 @@ def main():
|
||||||
layerbranch = layer.get_layerbranch(branch)
|
layerbranch = layer.get_layerbranch(branch)
|
||||||
if layerbranch:
|
if layerbranch:
|
||||||
last_rev[layerbranch] = layerbranch.vcs_last_rev
|
last_rev[layerbranch] = layerbranch.vcs_last_rev
|
||||||
|
layerupdate.layerbranch = layerbranch
|
||||||
|
layerupdate.log = output
|
||||||
|
if not options.dryrun:
|
||||||
|
layerupdate.save()
|
||||||
|
|
||||||
if ret == 254:
|
if ret == 254:
|
||||||
# Interrupted by user, break out of loop
|
# Interrupted by user, break out of loop
|
||||||
|
@ -246,11 +293,20 @@ def main():
|
||||||
finally:
|
finally:
|
||||||
layerconfparser.shutdown()
|
layerconfparser.shutdown()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
utils.unlock_file(lockfile)
|
utils.unlock_file(lockfile)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
update.log = ''.join(listhandler.read())
|
||||||
|
update.finished = datetime.now()
|
||||||
|
if not options.dryrun:
|
||||||
|
update.save()
|
||||||
|
|
||||||
|
if not options.dryrun:
|
||||||
|
# Purge old update records
|
||||||
|
update_purge_days = getattr(settings, 'UPDATE_PURGE_DAYS', 30)
|
||||||
|
Update.objects.filter(started__lte=datetime.now()-timedelta(days=update_purge_days)).delete()
|
||||||
|
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ from django.conf.urls import *
|
||||||
from django.views.generic import TemplateView, DetailView, ListView, RedirectView
|
from django.views.generic import TemplateView, DetailView, ListView, RedirectView
|
||||||
from django.views.defaults import page_not_found
|
from django.views.defaults import page_not_found
|
||||||
from django.core.urlresolvers import reverse_lazy
|
from django.core.urlresolvers import reverse_lazy
|
||||||
from layerindex.views import LayerListView, LayerReviewListView, LayerReviewDetailView, RecipeSearchView, MachineSearchView, PlainTextListView, LayerDetailView, edit_layer_view, delete_layer_view, edit_layernote_view, delete_layernote_view, HistoryListView, EditProfileFormView, AdvancedRecipeSearchView, BulkChangeView, BulkChangeSearchView, bulk_change_edit_view, bulk_change_patch_view, BulkChangeDeleteView, RecipeDetailView, RedirectParamsView, ClassicRecipeSearchView, ClassicRecipeDetailView, ClassicRecipeStatsView
|
from layerindex.views import LayerListView, LayerReviewListView, LayerReviewDetailView, RecipeSearchView, MachineSearchView, PlainTextListView, LayerDetailView, edit_layer_view, delete_layer_view, edit_layernote_view, delete_layernote_view, HistoryListView, EditProfileFormView, AdvancedRecipeSearchView, BulkChangeView, BulkChangeSearchView, bulk_change_edit_view, bulk_change_patch_view, BulkChangeDeleteView, RecipeDetailView, RedirectParamsView, ClassicRecipeSearchView, ClassicRecipeDetailView, ClassicRecipeStatsView, LayerUpdateDetailView, UpdateListView, UpdateDetailView
|
||||||
from layerindex.models import LayerItem, Recipe, RecipeChangeset
|
from layerindex.models import LayerItem, Recipe, RecipeChangeset
|
||||||
from rest_framework import routers
|
from rest_framework import routers
|
||||||
from . import restviews
|
from . import restviews
|
||||||
|
@ -67,6 +67,10 @@ urlpatterns = patterns('',
|
||||||
template_name='layerindex/recipedetail.html'),
|
template_name='layerindex/recipedetail.html'),
|
||||||
name='recipe'),
|
name='recipe'),
|
||||||
url(r'^layer/(?P<name>[-\w]+)/publish/$', 'layerindex.views.publish', name="publish"),
|
url(r'^layer/(?P<name>[-\w]+)/publish/$', 'layerindex.views.publish', name="publish"),
|
||||||
|
url(r'^layerupdate/(?P<pk>[-\w]+)/$',
|
||||||
|
LayerUpdateDetailView.as_view(
|
||||||
|
template_name='layerindex/layerupdate.html'),
|
||||||
|
name='layerupdate'),
|
||||||
url(r'^bulkchange/$',
|
url(r'^bulkchange/$',
|
||||||
BulkChangeView.as_view(
|
BulkChangeView.as_view(
|
||||||
template_name='layerindex/bulkchange.html'),
|
template_name='layerindex/bulkchange.html'),
|
||||||
|
@ -97,6 +101,14 @@ urlpatterns = patterns('',
|
||||||
# context_object_name='recipe_list',
|
# context_object_name='recipe_list',
|
||||||
# template_name='layerindex/rawrecipes.txt'),
|
# template_name='layerindex/rawrecipes.txt'),
|
||||||
# name='recipe_list_raw'),
|
# name='recipe_list_raw'),
|
||||||
|
url(r'^updates/$',
|
||||||
|
UpdateListView.as_view(
|
||||||
|
template_name='layerindex/updatelist.html'),
|
||||||
|
name='update_list'),
|
||||||
|
url(r'^updates/(?P<pk>[-\w]+)/$',
|
||||||
|
UpdateDetailView.as_view(
|
||||||
|
template_name='layerindex/updatedetail.html'),
|
||||||
|
name='update'),
|
||||||
url(r'^history/$',
|
url(r'^history/$',
|
||||||
HistoryListView.as_view(
|
HistoryListView.as_view(
|
||||||
template_name='layerindex/history.html'),
|
template_name='layerindex/history.html'),
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
# layerindex-web - Branch-based URL definitions
|
# layerindex-web - Branch-based URL definitions
|
||||||
#
|
#
|
||||||
# Copyright (C) 2013 Intel Corporation
|
# Copyright (C) 2013-2016 Intel Corporation
|
||||||
#
|
#
|
||||||
# Licensed under the MIT license, see COPYING.MIT for details
|
# Licensed under the MIT license, see COPYING.MIT for details
|
||||||
|
|
||||||
from django.conf.urls import *
|
from django.conf.urls import *
|
||||||
from django.views.defaults import page_not_found
|
from django.views.defaults import page_not_found
|
||||||
from django.core.urlresolvers import reverse_lazy
|
from django.core.urlresolvers import reverse_lazy
|
||||||
from layerindex.views import LayerListView, RecipeSearchView, MachineSearchView, DistroSearchView, PlainTextListView, LayerDetailView, edit_layer_view, delete_layer_view, edit_layernote_view, delete_layernote_view, RedirectParamsView, DuplicatesView
|
from layerindex.views import LayerListView, RecipeSearchView, MachineSearchView, DistroSearchView, PlainTextListView, LayerDetailView, edit_layer_view, delete_layer_view, edit_layernote_view, delete_layernote_view, RedirectParamsView, DuplicatesView, LayerUpdateDetailView
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
url(r'^$',
|
url(r'^$',
|
||||||
|
|
|
@ -223,6 +223,19 @@ def logger_create(name):
|
||||||
logger.setLevel(logging.INFO)
|
logger.setLevel(logging.INFO)
|
||||||
return logger
|
return logger
|
||||||
|
|
||||||
|
class ListHandler(logging.Handler):
|
||||||
|
"""Logging handler which accumulates formatted log records in a list, returning the list on demand"""
|
||||||
|
def __init__(self):
|
||||||
|
self.log = []
|
||||||
|
logging.Handler.__init__(self, logging.WARNING)
|
||||||
|
def emit(self, record):
|
||||||
|
self.log.append('%s\n' % self.format(record))
|
||||||
|
def read(self):
|
||||||
|
log = self.log
|
||||||
|
self.log = []
|
||||||
|
return log
|
||||||
|
|
||||||
|
|
||||||
def lock_file(fn):
|
def lock_file(fn):
|
||||||
starttime = time.time()
|
starttime = time.time()
|
||||||
while True:
|
while True:
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# layerindex-web - view definitions
|
# layerindex-web - view definitions
|
||||||
#
|
#
|
||||||
# Copyright (C) 2013-2014 Intel Corporation
|
# Copyright (C) 2013-2016 Intel Corporation
|
||||||
#
|
#
|
||||||
# Licensed under the MIT license, see COPYING.MIT for details
|
# Licensed under the MIT license, see COPYING.MIT for details
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidde
|
||||||
from django.core.urlresolvers import reverse, reverse_lazy, resolve
|
from django.core.urlresolvers import reverse, reverse_lazy, resolve
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.template import RequestContext
|
from django.template import RequestContext
|
||||||
from layerindex.models import Branch, LayerItem, LayerMaintainer, LayerBranch, LayerDependency, LayerNote, Recipe, Machine, Distro, BBClass, BBAppend, RecipeChange, RecipeChangeset, ClassicRecipe
|
from layerindex.models import Branch, LayerItem, LayerMaintainer, LayerBranch, LayerDependency, LayerNote, Update, LayerUpdate, Recipe, Machine, Distro, BBClass, BBAppend, RecipeChange, RecipeChangeset, ClassicRecipe
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from django.views.generic import TemplateView, DetailView, ListView
|
from django.views.generic import TemplateView, DetailView, ListView
|
||||||
from django.views.generic.edit import CreateView, DeleteView, UpdateView
|
from django.views.generic.edit import CreateView, DeleteView, UpdateView
|
||||||
|
@ -18,7 +18,7 @@ from django.views.generic.base import RedirectView
|
||||||
from layerindex.forms import EditLayerForm, LayerMaintainerFormSet, EditNoteForm, EditProfileForm, RecipeChangesetForm, AdvancedRecipeSearchForm, BulkChangeEditFormSet, ClassicRecipeForm, ClassicRecipeSearchForm
|
from layerindex.forms import EditLayerForm, LayerMaintainerFormSet, EditNoteForm, EditProfileForm, RecipeChangesetForm, AdvancedRecipeSearchForm, BulkChangeEditFormSet, ClassicRecipeForm, ClassicRecipeSearchForm
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.contrib.auth.models import User, Permission
|
from django.contrib.auth.models import User, Permission
|
||||||
from django.db.models import Q, Count
|
from django.db.models import Q, Count, Sum
|
||||||
from django.core.mail import EmailMessage
|
from django.core.mail import EmailMessage
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
from django.template import Context
|
from django.template import Context
|
||||||
|
@ -328,6 +328,7 @@ class LayerDetailView(DetailView):
|
||||||
context['distros'] = layerbranch.distro_set.order_by('name')
|
context['distros'] = layerbranch.distro_set.order_by('name')
|
||||||
context['appends'] = layerbranch.bbappend_set.order_by('filename')
|
context['appends'] = layerbranch.bbappend_set.order_by('filename')
|
||||||
context['classes'] = layerbranch.bbclass_set.order_by('name')
|
context['classes'] = layerbranch.bbclass_set.order_by('name')
|
||||||
|
context['updates'] = layerbranch.layerupdate_set.order_by('-started')
|
||||||
context['url_branch'] = self.kwargs['branch']
|
context['url_branch'] = self.kwargs['branch']
|
||||||
context['this_url_name'] = resolve(self.request.path_info).url_name
|
context['this_url_name'] = resolve(self.request.path_info).url_name
|
||||||
return context
|
return context
|
||||||
|
@ -599,6 +600,29 @@ class MachineSearchView(ListView):
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateListView(ListView):
|
||||||
|
context_object_name = "updates"
|
||||||
|
paginate_by = 50
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return Update.objects.all().order_by('-started').annotate(errors=Sum('layerupdate__errors'), warnings=Sum('layerupdate__warnings'))
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateDetailView(DetailView):
|
||||||
|
model = Update
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(UpdateDetailView, self).get_context_data(**kwargs)
|
||||||
|
update = self.get_object()
|
||||||
|
if update:
|
||||||
|
context['layerupdates'] = update.layerupdate_set.exclude(log__isnull=True).exclude(log__exact='')
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class LayerUpdateDetailView(DetailView):
|
||||||
|
model = LayerUpdate
|
||||||
|
|
||||||
|
|
||||||
class DistroSearchView(ListView):
|
class DistroSearchView(ListView):
|
||||||
context_object_name = 'distro_list'
|
context_object_name = 'distro_list'
|
||||||
paginate_by = 50
|
paginate_by = 50
|
||||||
|
|
|
@ -211,6 +211,9 @@ BITBAKE_REPO_URL = "git://git.openembedded.org/bitbake"
|
||||||
# Core layer to be used by the update script for basic BitBake configuration
|
# Core layer to be used by the update script for basic BitBake configuration
|
||||||
CORE_LAYER_NAME = "openembedded-core"
|
CORE_LAYER_NAME = "openembedded-core"
|
||||||
|
|
||||||
|
# Update records older than this number of days will be deleted every update
|
||||||
|
UPDATE_PURGE_DAYS = 30
|
||||||
|
|
||||||
# Settings for layer submission feature
|
# Settings for layer submission feature
|
||||||
SUBMIT_EMAIL_FROM = 'noreply@example.com'
|
SUBMIT_EMAIL_FROM = 'noreply@example.com'
|
||||||
SUBMIT_EMAIL_SUBJECT = 'OE Layerindex layer submission'
|
SUBMIT_EMAIL_SUBJECT = 'OE Layerindex layer submission'
|
||||||
|
|
|
@ -72,6 +72,7 @@
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a href="{% url 'bulk_change' %}">Bulk Change</a></li>
|
<li><a href="{% url 'bulk_change' %}">Bulk Change</a></li>
|
||||||
<li><a href="{% url 'duplicates' 'master' %}">Duplicates</a></li>
|
<li><a href="{% url 'duplicates' 'master' %}">Duplicates</a></li>
|
||||||
|
<li><a href="{% url 'update_list' %}">Updates</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -181,6 +181,9 @@
|
||||||
{% if distros.count > 0 %}
|
{% if distros.count > 0 %}
|
||||||
<li><a href="#distros" data-toggle="tab">Distros</a></li>
|
<li><a href="#distros" data-toggle="tab">Distros</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if updates.count > 0 %}
|
||||||
|
<li><a href="#updates" data-toggle="tab">Updates</a></li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
|
@ -298,6 +301,40 @@
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if updates.count > 0 %}
|
||||||
|
<div class="tab-pane" id="updates">
|
||||||
|
<div class="navbar">
|
||||||
|
<div class="navbar-inner">
|
||||||
|
<a class="brand pull-left">{{ layeritem.name }} updates</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="table table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Date/time</th>
|
||||||
|
<th>Errors</th>
|
||||||
|
<th>Warnings</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for update in updates %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{% if update.log %}
|
||||||
|
<a href="{% url 'layerupdate' update.id %}">{{ update.started }}{% if update.update.reload %} (reload){% endif%}</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="muted">{{ update.started }}{% if update.update.reload %} (reload){% endif%}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{% if update.errors %}<span class="badge badge-important">{{ update.errors }}</span>{% endif %}</td>
|
||||||
|
<td>{% if update.warnings %}<span class="badge badge-warning">{{ update.warnings }}</span>{% endif %}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
30
templates/layerindex/layerupdate.html
Normal file
30
templates/layerindex/layerupdate.html
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% comment %}
|
||||||
|
|
||||||
|
layerindex-web - layer update page
|
||||||
|
|
||||||
|
Copyright (C) 2016 Intel Corporation
|
||||||
|
Licensed under the MIT license, see COPYING.MIT for details
|
||||||
|
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
<!--
|
||||||
|
{% block title_append %} - {{ layerupdate.layerbranch.layer.name }} {{ layerupdate.layerbranch.branch.name }} - {{ layerupdate.started }} {% endblock %}
|
||||||
|
-->
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% autoescape on %}
|
||||||
|
|
||||||
|
<h2>{{ layerupdate.layerbranch.layer.name }} {{ layerupdate.layerbranch.branch.name }} - {{ layerupdate.started }}</h2>
|
||||||
|
|
||||||
|
<pre>{{ layerupdate.log }}</pre>
|
||||||
|
|
||||||
|
{% endautoescape %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
{% endblock %}
|
47
templates/layerindex/updatedetail.html
Normal file
47
templates/layerindex/updatedetail.html
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% comment %}
|
||||||
|
|
||||||
|
layerindex-web - update page
|
||||||
|
|
||||||
|
Copyright (C) 2016 Intel Corporation
|
||||||
|
Licensed under the MIT license, see COPYING.MIT for details
|
||||||
|
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
<!--
|
||||||
|
{% block title_append %} - {{ update.started }} {% endblock %}
|
||||||
|
-->
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% autoescape on %}
|
||||||
|
|
||||||
|
<ul class="breadcrumb">
|
||||||
|
<li><a href="{% url 'update_list' %}">Updates</a> <span class="divider">→</span></li>
|
||||||
|
<li class="active">{{ update.started }}</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
|
<h2>{{ update.started }} {% if update.reload %}(reload){% endif %}</h2>
|
||||||
|
|
||||||
|
{% if update.log %}
|
||||||
|
<pre>{{ update.log }}</pre>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for layerupdate in layerupdates %}
|
||||||
|
<a href="{% url 'layer_item' layerupdate.layerbranch.branch.name layerupdate.layerbranch.layer.name %}"><h3>{{ layerupdate.layerbranch.layer.name }} {{ layerupdate.layerbranch.branch.name }}</h3></a>
|
||||||
|
<pre>{{ layerupdate.log }}</pre>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if not update.log and not layerupdates %}
|
||||||
|
<p>No messages</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endautoescape %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
{% endblock %}
|
57
templates/layerindex/updatelist.html
Normal file
57
templates/layerindex/updatelist.html
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% comment %}
|
||||||
|
|
||||||
|
layerindex-web - updates list page template
|
||||||
|
|
||||||
|
Copyright (C) 2016 Intel Corporation
|
||||||
|
Licensed under the MIT license, see COPYING.MIT for details
|
||||||
|
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
|
||||||
|
<!--
|
||||||
|
{% block title_append %} - updates{% endblock %}
|
||||||
|
-->
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% autoescape on %}
|
||||||
|
|
||||||
|
<div class="row-fluid">
|
||||||
|
<div class="span9 offset1">
|
||||||
|
|
||||||
|
<table class="table table-striped table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Update date</th>
|
||||||
|
<th>Time</th>
|
||||||
|
<th>Errors</th>
|
||||||
|
<th>Warnings</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{% for update in updates %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="{% url 'update' update.id %}">{{ update.started }}{% if update.reload %} (reload){% endif %}</a></td>
|
||||||
|
<td>{% if update.finished %}{{ update.started|timesince:update.finished }}{% else %}(in progress){% endif %}</td>
|
||||||
|
<td>{% if update.errors %}<span class="badge badge-important">{{ update.errors }}</span>{% endif %}</td>
|
||||||
|
<td>{% if update.warnings %}<span class="badge badge-warning">{{ update.warnings }}</span>{% endif %}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if is_paginated %}
|
||||||
|
{% load pagination %}
|
||||||
|
{% pagination page_obj %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endautoescape %}
|
||||||
|
|
||||||
|
{% endblock %}
|
Loading…
Reference in New Issue
Block a user