Skip to content

Commit d98054c

Browse files
committed
Added a new yang checker, 'yanglint', to the existing Yang checker class, in
addition to the existing 'pyang' checker. Added modal overlay displays showing the yang check results every place the yin/yang symbol is shown (red or green) to indicate the presencee and result of yang checks. Added a Yang Validation: line in the document meta-information section on the document's page in the datatracker. Added the result of the xym extaction to the yang check results, to make extration failures visible. Added the version of the used xym, pyang, and yanglint commands to the check results. Added an action to move successfully extracted and validated modules to the module library directories immediately on submission. Added the xym and pyang repositories as svn:external components, rather than listing them in requirements.txt, as there has been delays of many months between essential features in the repositories, and an actual release. We may get occasional buildbot failures if broken code is pulled in from the repository, but better that than the functionality failure of severely outdated componets. Added a new management command to re-run yang validation for active drafts for which yang modules were found at submission time, in order to pick up imported models which may have arrived in the model libraries after the draft's submission. Run daily from bin/daily. Added a table to hold version information for external commands. The yang checker output should include the version information of the used checkers, but seems unnecessary to run each command with its --version switch every time we check a module... Added a new management command to collect version information for external commands on demand. To be run daily from bin/daily. Added tests to verify that xym, pyang and yanglint information is available on the submission confirmation page, and updated the yang module contained in the test document to validate under both pyang and yanglint. Updated admin.py and resource.py files as needed. - Legacy-Id: 13630
1 parent c53e378 commit d98054c

29 files changed

Lines changed: 699 additions & 353 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,4 @@
3939
/static
4040
/testresult
4141
/unix.tag
42+
/tmp-nomcom-public-keys-dir

bin/daily

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,16 @@ logger -p user.info -t cron "Running $DTDIR/bin/daily"
1818
# Set up the virtual environment
1919
source $DTDIR/env/bin/activate
2020

21+
22+
# Update our information about the current version of some commands we use
23+
$DTDIR/ietf/manage.py update_external_command_info
24+
2125
# Populate the yang repositories
2226
$DTDIR/ietf/manage.py populate_yang_model_dirs
2327

28+
# Re-run yang checks on active documents
29+
$DTDIR/ietf/manage.py run_yang_model_checks
30+
2431
# Expire internet drafts
2532
# Enable when removed from /a/www/ietf-datatracker/scripts/Cron-runner:
2633
$DTDIR/ietf/bin/expire-ids
@@ -36,5 +43,3 @@ $DTDIR/ietf/bin/rfc-editor-index-updates -d 1969-01-01
3643

3744
# Fetch meeting attendance data from ietf.org/registration/attendees
3845
$DTDIR/ietf/manage.py fetch_meeting_attendance --latest
39-
40-

ietf/doc/templatetags/ietf_filters.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,3 +533,6 @@ def comma_separated_list(seq, end_word="and"):
533533
def role_names(roles):
534534
return list(set([ "%s %s" % (r.group.name, r.name.name) for r in roles ]))
535535

536+
@register.filter()
537+
def zaptmp(s):
538+
return re.sub(r'/tmp/tmp[^/]+/', '', s)

ietf/doc/views_doc.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1277,3 +1277,11 @@ def all_presentations(request, name):
12771277
'in_progress': in_progress,
12781278
'past' : past,
12791279
})
1280+
1281+
def document_checks(request, name):
1282+
doc = get_object_or_404(Document, docalias__name=name, type_id='draft')
1283+
1284+
checks = doc.submission.latest_checks()
1285+
debug.show('checks')
1286+
1287+
return render(request, 'doc/document_checks.html', {'doc': doc, 'checks': checks})

ietf/name/fixtures/names.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9708,5 +9708,38 @@
97089708
},
97099709
"model": "name.topicaudiencename",
97109710
"pk": "nominees"
9711+
},
9712+
{
9713+
"fields": {
9714+
"command": "xym",
9715+
"switch": "--version",
9716+
"time": "2017-06-15T06:24:58.869",
9717+
"used": true,
9718+
"version": "xym 0.3.2"
9719+
},
9720+
"model": "utils.versioninfo",
9721+
"pk": 1
9722+
},
9723+
{
9724+
"fields": {
9725+
"command": "pyang",
9726+
"switch": "--version",
9727+
"time": "2017-06-15T06:24:59.516",
9728+
"used": true,
9729+
"version": "pyang 1.7.2"
9730+
},
9731+
"model": "utils.versioninfo",
9732+
"pk": 2
9733+
},
9734+
{
9735+
"fields": {
9736+
"command": "yanglint",
9737+
"switch": "--version",
9738+
"time": "2017-06-15T06:24:59.531",
9739+
"used": true,
9740+
"version": "yanglint 0.12.183"
9741+
},
9742+
"model": "utils.versioninfo",
9743+
"pk": 3
97119744
}
97129745
]

ietf/name/generate_fixtures.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,8 @@ def output(name, seq):
4747
objects += ietf.mailtrigger.models.Recipient.objects.all()
4848
objects += ietf.mailtrigger.models.MailTrigger.objects.all()
4949

50+
import ietf.utils.models
51+
objects += ietf.utils.models.VersionInfo.objects.all()
52+
5053
output("names", objects)
5154

ietf/settings.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -660,11 +660,19 @@ def skip_unreadable_post(record):
660660
IDSUBMIT_DEFAULT_CUTOFF_TIME_UTC = datetime.timedelta(hours=23, minutes=59, seconds=59)
661661
IDSUBMIT_DEFAULT_CUTOFF_WARNING_DAYS = datetime.timedelta(days=21)
662662

663+
# 14 Jun 2017: New convention: prefix settings with the app name to which
664+
# they (mainly) belong. So here, SUBMIT_, rather than IDSUBMIT_
665+
SUBMIT_YANG_RFC_MODEL_DIR = '/a/www/ietf-ftp/yang/rfcmod/'
666+
SUBMIT_YANG_DRAFT_MODEL_DIR = '/a/www/ietf-ftp/yang/draftmod/'
667+
SUBMIT_YANG_INVAL_MODEL_DIR = '/a/www/ietf-ftp/yang/invalmod/'
668+
663669
IDSUBMIT_REPOSITORY_PATH = INTERNET_DRAFT_PATH
664670
IDSUBMIT_STAGING_PATH = '/a/www/www6s/staging/'
665671
IDSUBMIT_STAGING_URL = '//www.ietf.org/staging/'
666672
IDSUBMIT_IDNITS_BINARY = '/a/www/ietf-datatracker/scripts/idnits'
667-
IDSUBMIT_PYANG_COMMAND = 'pyang -p %(modpath)s --verbose --ietf %(model)s'
673+
SUBMIT_PYANG_COMMAND = 'pyang --verbose --ietf -p {libs} {model}'
674+
SUBMIT_YANGLINT_COMMAND = 'yanglint --verbose -p {rfclib} -p {draftlib} {model}'
675+
SUBMIT_YANGLINT_COMMAND = None # use the value above if you have yanglint installed
668676

669677
IDSUBMIT_CHECKER_CLASSES = (
670678
"ietf.submit.checkers.DraftIdnitsChecker",
@@ -696,10 +704,6 @@ def skip_unreadable_post(record):
696704
IDSUBMIT_MAX_DAILY_SUBMISSIONS = 1000
697705
IDSUBMIT_MAX_DAILY_SUBMISSIONS_SIZE = 2000 # in MB
698706

699-
YANG_RFC_MODEL_DIR = '/a/www/ietf-ftp/yang/rfcmod/'
700-
YANG_DRAFT_MODEL_DIR = '/a/www/ietf-ftp/yang/draftmod/'
701-
YANG_INVAL_MODEL_DIR = '/a/www/ietf-ftp/yang/invalmod/'
702-
703707
XML_LIBRARY = "/www/tools.ietf.org/tools/xml2rfc/web/public/rfc/"
704708

705709
# === Meeting Related Settings =================================================

ietf/static/ietf/css/ietf.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -865,3 +865,9 @@ blockquote {
865865
#debug-query-table .code .current {
866866
background-color: #ddd;
867867
}
868+
869+
.checker-warning,
870+
.checker-success {
871+
line-height: 1.0;
872+
cursor: pointer;
873+
}

ietf/submit/admin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def rev(self, instance):
3333
admin.site.register(SubmissionEvent, SubmissionEventAdmin)
3434

3535
class SubmissionCheckAdmin(admin.ModelAdmin):
36-
list_display = ['submission', 'time', 'checker', 'passed', 'errors', 'warnings', 'items']
36+
list_display = ['submission', 'time', 'checker', 'passed', 'errors', 'warnings', 'message']
3737
raw_id_fields = ['submission']
3838
search_fields = ['submission__name']
3939
admin.site.register(SubmissionCheck, SubmissionCheckAdmin)

ietf/submit/checkers.py

Lines changed: 91 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,19 @@
22

33
import os
44
import re
5+
import sys
56
from xym import xym
67
import shutil
78
import tempfile
9+
import StringIO
810

911
from django.conf import settings
1012

1113
import debug # pyflakes:ignore
1214

13-
from ietf.utils.pipe import pipe
1415
from ietf.utils.log import log
16+
from ietf.utils.models import VersionInfo
17+
from ietf.utils.pipe import pipe
1518

1619
class DraftSubmissionChecker():
1720
name = ""
@@ -47,6 +50,9 @@ class DraftIdnitsChecker(object):
4750
# start using this when we provide more in the way of warnings during
4851
# submission checking:
4952
# symbol = '<span class="fa fa-check-square"></span>'
53+
# symbol = u'<span class="large">\ua17d</span>' # Yi syllable 'nit'
54+
# symbol = u'<span class="large">\ub2e1</span>' # Hangul syllable 'nit'
55+
5056
symbol = ""
5157

5258
def __init__(self, options=["--submitcheck", "--nitcount", ]):
@@ -123,39 +129,66 @@ class DraftYangChecker(object):
123129
def check_file_txt(self, path):
124130
name = os.path.basename(path)
125131
workdir = tempfile.mkdtemp()
126-
errors = []
127-
warnings = []
128-
results = {}
132+
errors = 0
133+
warnings = 0
134+
message = ""
135+
results = []
136+
passed = True # Used by the submission tool. Yang checks always pass.
129137

130-
extractor = xym.YangModuleExtractor(path, workdir, strict=True, debug_level = 0)
138+
extractor = xym.YangModuleExtractor(path, workdir, strict=True, strict_examples=False, debug_level=0)
131139
if not os.path.exists(path):
132140
return None, "%s: No such file or directory: '%s'"%(name.capitalize(), path), errors, warnings, results
133141
with open(path) as file:
142+
out = ""
143+
err = ""
144+
code = 0
134145
try:
135146
# This places the yang models as files in workdir
147+
saved_stdout = sys.stdout
148+
saved_stderr = sys.stderr
149+
sys.stdout = StringIO.StringIO()
150+
sys.stderr = StringIO.StringIO()
136151
extractor.extract_yang_model(file.readlines())
152+
out = sys.stdout.getvalue()
153+
err = sys.stderr.getvalue()
154+
sys.stdout = saved_stdout
155+
sys.stderr = saved_stderr
137156
model_list = extractor.get_extracted_models()
138157
except Exception as exc:
139-
passed = False
140-
message = exc
141-
errors = [ (name, None, None, exc) ]
142-
warnings = []
143-
return passed, message, errors, warnings
158+
code = 1
159+
err = '\n'.join( [ m for m in [out, err, exc] if m ] )
160+
if err:
161+
code += 1
162+
command = "xym"
163+
cmd_version = VersionInfo.objects.get(command=command).version
164+
message = "%s:\n%s\n\n" % (cmd_version, out.replace('\n\n','\n').strip() if code == 0 else err)
165+
166+
results.append({
167+
"name": name,
168+
"passed": passed,
169+
"message": message,
170+
"warnings": 0,
171+
"errors": code,
172+
"items": [],
173+
})
144174

145175
for model in model_list:
146176
path = os.path.join(workdir, model)
177+
message = ""
147178
modpath = ':'.join([
148179
workdir,
149-
settings.YANG_RFC_MODEL_DIR,
150-
settings.YANG_DRAFT_MODEL_DIR,
151-
settings.YANG_INVAL_MODEL_DIR,
180+
settings.SUBMIT_YANG_RFC_MODEL_DIR,
181+
settings.SUBMIT_YANG_DRAFT_MODEL_DIR,
182+
settings.SUBMIT_YANG_INVAL_MODEL_DIR,
152183
])
153184
with open(path) as file:
154185
text = file.readlines()
155-
cmd = settings.IDSUBMIT_PYANG_COMMAND % {"modpath": modpath, "model": path, }
186+
# pyang
187+
cmd_template = settings.SUBMIT_PYANG_COMMAND
188+
command = cmd_template.split()[0]
189+
cmd_version = VersionInfo.objects.get(command=command).version
190+
cmd = cmd_template.format(libs=modpath, model=path)
156191
code, out, err = pipe(cmd)
157-
errors = 0
158-
warnings = 0
159192
items = []
160193
if code > 0:
161194
error_lines = err.splitlines()
@@ -175,26 +208,54 @@ def check_file_txt(self, path):
175208
warnings += 1
176209
except ValueError:
177210
pass
178-
results[model] = {
179-
"passed": code == 0,
180-
"message": out+"No validation errors\n" if code == 0 else err,
211+
#passed = passed and code == 0 # For the submission tool. Yang checks always pass
212+
message += "%s: %s:\n%s\n" % (cmd_version, cmd_template, out+"No validation errors\n" if code == 0 else err)
213+
214+
# yanglint
215+
cmd_template = settings.SUBMIT_YANGLINT_COMMAND
216+
command = cmd_template.split()[0]
217+
cmd_version = VersionInfo.objects.get(command=command).version
218+
cmd = cmd_template.format(model=path, rfclib=settings.SUBMIT_YANG_RFC_MODEL_DIR, draftlib=settings.SUBMIT_YANG_DRAFT_MODEL_DIR)
219+
code, out, err = pipe(cmd)
220+
if code > 0:
221+
error_lines = err.splitlines()
222+
for line in error_lines:
223+
if line.strip():
224+
try:
225+
if 'err : ' in line:
226+
errors += 1
227+
if 'warn: ' in line:
228+
warnings += 1
229+
except ValueError:
230+
pass
231+
#passed = passed and code == 0 # For the submission tool. Yang checks always pass
232+
message += "%s: %s:\n%s\n" % (cmd_version, cmd_template, out+"No validation errors\n" if code == 0 else err)
233+
234+
if errors==0 and warnings==0:
235+
dest = os.path.join(settings.SUBMIT_YANG_DRAFT_MODEL_DIR, model)
236+
shutil.move(path, dest)
237+
else:
238+
dest = os.path.join(settings.SUBMIT_YANG_INVAL_MODEL_DIR, model)
239+
shutil.move(path, dest)
240+
241+
# summary result
242+
results.append({
243+
"name": model,
244+
"passed": passed,
245+
"message": message,
181246
"warnings": warnings,
182247
"errors": errors,
183248
"items": items,
184-
}
249+
})
250+
185251

186252
shutil.rmtree(workdir)
187253

188-
## For now, never fail because of failed yang validation.
189-
if len(model_list):
190-
passed = True
191-
else:
192-
passed = None
193-
#passed = all( res["passed"] for res in results.values() )
194-
message = "\n\n".join([ "\n".join([model+':', res["message"]]) for model, res in results.items() ])
195-
errors = sum(res["errors"] for res in results.values() )
196-
warnings = sum(res["warnings"] for res in results.values() )
197-
items = [ e for res in results.values() for e in res["items"] ]
254+
passed = all( res["passed"] for res in results )
255+
message = "\n".join([ "\n".join([res['name']+':', res["message"]]) for res in results ])
256+
errors = sum(res["errors"] for res in results )
257+
warnings = sum(res["warnings"] for res in results )
258+
items = [ e for res in results for e in res["items"] ]
198259

199260
return passed, message, errors, warnings, items
200261

0 commit comments

Comments
 (0)