Skip to content

Commit 92784f9

Browse files
feat: re-run yang checks via celery (ietf-tools#7558)
* refactor: yang checks -> task * chore: add periodic task * chore: remove run_yang_model_checks.py * test: add tests * refactor: populate_yang_model_dirs -> task * chore: remove populate_yang_model_dirs.py * chore: remove python setup from bin/daily
1 parent 0ac2ae1 commit 92784f9

7 files changed

Lines changed: 178 additions & 269 deletions

File tree

bin/daily

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
# This script is expected to be triggered by cron from
66
# /etc/cron.d/datatracker
77
export LANG=en_US.UTF-8
8-
export PYTHONIOENCODING=utf-8
98

109
# Make sure we stop if something goes wrong:
1110
program=${0##*/}
@@ -17,10 +16,6 @@ cd $DTDIR/
1716

1817
logger -p user.info -t cron "Running $DTDIR/bin/daily"
1918

20-
# Set up the virtual environment
21-
source $DTDIR/env/bin/activate
22-
23-
2419
# Get IANA-registered yang models
2520
#YANG_IANA_DIR=$(python -c 'import ietf.settings; print ietf.settings.SUBMIT_YANG_IANA_MODEL_DIR')
2621
# Hardcode the rsync target to avoid any unwanted deletes:
@@ -30,9 +25,3 @@ rsync -avzq --delete /a/www/ietf-ftp/iana/yang-parameters/ /a/www/ietf-ftp/yang/
3025
# Get Yang models from Yangcatalog.
3126
#rsync -avzq rsync://rsync.yangcatalog.org:10873/yangdeps /a/www/ietf-ftp/yang/catalogmod/
3227
/a/www/ietf-datatracker/scripts/sync_to_yangcatalog
33-
34-
# Populate the yang repositories
35-
$DTDIR/ietf/manage.py populate_yang_model_dirs -v0
36-
37-
# Re-run yang checks on active documents
38-
$DTDIR/ietf/manage.py run_yang_model_checks -v0

ietf/submit/tasks.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010

1111
from ietf.submit.models import Submission
1212
from ietf.submit.utils import (cancel_submission, create_submission_event, process_uploaded_submission,
13-
process_and_accept_uploaded_submission)
13+
process_and_accept_uploaded_submission, run_all_yang_model_checks,
14+
populate_yang_model_dirs)
1415
from ietf.utils import log
1516

1617

@@ -66,6 +67,12 @@ def cancel_stale_submissions():
6667
create_submission_event(None, subm, 'Submission canceled: expired without being posted')
6768

6869

70+
@shared_task
71+
def run_yang_model_checks_task():
72+
populate_yang_model_dirs()
73+
run_all_yang_model_checks()
74+
75+
6976
@shared_task(bind=True)
7077
def poke(self):
7178
log.log(f'Poked {self.name}, request id {self.request.id}')

ietf/submit/tests.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
from ietf.submit.forms import SubmissionBaseUploadForm, SubmissionAutoUploadForm
5050
from ietf.submit.models import Submission, Preapproval, SubmissionExtResource
5151
from ietf.submit.tasks import cancel_stale_submissions, process_and_accept_uploaded_submission_task
52+
from ietf.submit.utils import apply_yang_checker_to_draft, run_all_yang_model_checks
5253
from ietf.utils import tool_version
5354
from ietf.utils.accesstoken import generate_access_token
5455
from ietf.utils.mail import outbox, get_payload_text
@@ -3487,3 +3488,28 @@ def test_submission_checks(self):
34873488
"Your Internet-Draft failed at least one submission check.",
34883489
status_code=200,
34893490
)
3491+
3492+
3493+
class YangCheckerTests(TestCase):
3494+
@mock.patch("ietf.submit.utils.apply_yang_checker_to_draft")
3495+
def test_run_all_yang_model_checks(self, mock_apply):
3496+
active_drafts = WgDraftFactory.create_batch(3)
3497+
WgDraftFactory(states=[("draft", "expired")])
3498+
run_all_yang_model_checks()
3499+
self.assertEqual(mock_apply.call_count, 3)
3500+
self.assertCountEqual(
3501+
[args[0][1] for args in mock_apply.call_args_list],
3502+
active_drafts,
3503+
)
3504+
3505+
def test_apply_yang_checker_to_draft(self):
3506+
draft = WgDraftFactory()
3507+
submission = SubmissionFactory(name=draft.name, rev=draft.rev)
3508+
submission.checks.create(checker="my-checker")
3509+
checker = mock.Mock()
3510+
checker.name = "my-checker"
3511+
checker.symbol = "X"
3512+
checker.check_file_txt.return_value = (True, "whee", None, None, {})
3513+
apply_yang_checker_to_draft(checker, draft)
3514+
self.assertEqual(checker.check_file_txt.call_args, mock.call(draft.get_file_name()))
3515+

ietf/submit/utils.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44

55
import datetime
66
import io
7+
import json
78
import os
89
import pathlib
910
import re
11+
import sys
1012
import time
1113
import traceback
1214
import xml2rfc
@@ -15,6 +17,7 @@
1517
from shutil import move
1618
from typing import Optional, Union # pyflakes:ignore
1719
from unidecode import unidecode
20+
from xym import xym
1821

1922
from django.conf import settings
2023
from django.core.exceptions import ValidationError
@@ -43,6 +46,7 @@
4346
from ietf.community.utils import update_name_contains_indexes_with_new_doc
4447
from ietf.submit.mail import ( announce_to_lists, announce_new_version, announce_to_authors,
4548
send_approval_request, send_submission_confirmation, announce_new_wg_00, send_manual_post_request )
49+
from ietf.submit.checkers import DraftYangChecker
4650
from ietf.submit.models import ( Submission, SubmissionEvent, Preapproval, DraftSubmissionStateName,
4751
SubmissionCheck, SubmissionExtResource )
4852
from ietf.utils import log
@@ -1431,3 +1435,133 @@ def process_uploaded_submission(submission):
14311435
submission.state_id = "uploaded"
14321436
submission.save()
14331437
create_submission_event(None, submission, desc="Completed submission validation checks")
1438+
1439+
1440+
def apply_yang_checker_to_draft(checker, draft):
1441+
submission = Submission.objects.filter(name=draft.name, rev=draft.rev).order_by('-id').first()
1442+
if submission:
1443+
check = submission.checks.filter(checker=checker.name).order_by('-id').first()
1444+
if check:
1445+
result = checker.check_file_txt(draft.get_file_name())
1446+
passed, message, errors, warnings, items = result
1447+
items = json.loads(json.dumps(items))
1448+
new_res = (passed, errors, warnings, message)
1449+
old_res = (check.passed, check.errors, check.warnings, check.message) if check else ()
1450+
if new_res != old_res:
1451+
log.log(f"Saving new yang checker results for {draft.name}-{draft.rev}")
1452+
qs = submission.checks.filter(checker=checker.name).order_by('time')
1453+
submission.checks.filter(checker=checker.name).exclude(pk=qs.first().pk).delete()
1454+
submission.checks.create(submission=submission, checker=checker.name, passed=passed,
1455+
message=message, errors=errors, warnings=warnings, items=items,
1456+
symbol=checker.symbol)
1457+
else:
1458+
log.log(f"Could not run yang checker for {draft.name}-{draft.rev}: missing submission object")
1459+
1460+
1461+
def run_all_yang_model_checks():
1462+
checker = DraftYangChecker()
1463+
for draft in Document.objects.filter(
1464+
type_id="draft",
1465+
states=State.objects.get(type="draft", slug="active"),
1466+
):
1467+
apply_yang_checker_to_draft(checker, draft)
1468+
1469+
1470+
def populate_yang_model_dirs():
1471+
"""Update the yang model dirs
1472+
1473+
* All yang modules from published RFCs should be extracted and be
1474+
available in an rfc-yang repository.
1475+
1476+
* All valid yang modules from active, not replaced, Internet-Drafts
1477+
should be extracted and be available in a draft-valid-yang repository.
1478+
1479+
* All, valid and invalid, yang modules from active, not replaced,
1480+
Internet-Drafts should be available in a draft-all-yang repository.
1481+
(Actually, given precedence ordering, it would be enough to place
1482+
non-validating modules in a draft-invalid-yang repository instead).
1483+
1484+
* In all cases, example modules should be excluded.
1485+
1486+
* Precedence is established by the search order of the repository as
1487+
provided to pyang.
1488+
1489+
* As drafts expire, models should be removed in order to catch cases
1490+
where a module being worked on depends on one which has slipped out
1491+
of the work queue.
1492+
1493+
"""
1494+
def extract_from(file, dir, strict=True):
1495+
saved_stdout = sys.stdout
1496+
saved_stderr = sys.stderr
1497+
xymerr = io.StringIO()
1498+
xymout = io.StringIO()
1499+
sys.stderr = xymerr
1500+
sys.stdout = xymout
1501+
model_list = []
1502+
try:
1503+
model_list = xym.xym(str(file), str(file.parent), str(dir), strict=strict, debug_level=-2)
1504+
for name in model_list:
1505+
modfile = moddir / name
1506+
mtime = file.stat().st_mtime
1507+
os.utime(str(modfile), (mtime, mtime))
1508+
if '"' in name:
1509+
name = name.replace('"', '')
1510+
modfile.rename(str(moddir / name))
1511+
model_list = [n.replace('"', '') for n in model_list]
1512+
except Exception as e:
1513+
log.log("Error when extracting from %s: %s" % (file, str(e)))
1514+
finally:
1515+
sys.stdout = saved_stdout
1516+
sys.stderr = saved_stderr
1517+
return model_list
1518+
1519+
# Extract from new RFCs
1520+
1521+
rfcdir = Path(settings.RFC_PATH)
1522+
1523+
moddir = Path(settings.SUBMIT_YANG_RFC_MODEL_DIR)
1524+
if not moddir.exists():
1525+
moddir.mkdir(parents=True)
1526+
1527+
latest = 0
1528+
for item in moddir.iterdir():
1529+
if item.stat().st_mtime > latest:
1530+
latest = item.stat().st_mtime
1531+
1532+
log.log(f"Extracting RFC Yang models to {moddir} ...")
1533+
for item in rfcdir.iterdir():
1534+
if item.is_file() and item.name.startswith('rfc') and item.name.endswith('.txt') and item.name[3:-4].isdigit():
1535+
if item.stat().st_mtime > latest:
1536+
model_list = extract_from(item, moddir)
1537+
for name in model_list:
1538+
if not (name.startswith('ietf') or name.startswith('iana')):
1539+
modfile = moddir / name
1540+
modfile.unlink()
1541+
1542+
# Extract valid modules from drafts
1543+
1544+
six_months_ago = time.time() - 6 * 31 * 24 * 60 * 60
1545+
1546+
def active(dirent):
1547+
return dirent.stat().st_mtime > six_months_ago
1548+
1549+
draftdir = Path(settings.INTERNET_DRAFT_PATH)
1550+
moddir = Path(settings.SUBMIT_YANG_DRAFT_MODEL_DIR)
1551+
if not moddir.exists():
1552+
moddir.mkdir(parents=True)
1553+
log.log(f"Emptying {moddir} ...")
1554+
for item in moddir.iterdir():
1555+
item.unlink()
1556+
1557+
log.log(f"Extracting draft Yang models to {moddir} ...")
1558+
for item in draftdir.iterdir():
1559+
try:
1560+
if item.is_file() and item.name.startswith('draft') and item.name.endswith('.txt') and active(item):
1561+
model_list = extract_from(item, moddir, strict=False)
1562+
for name in model_list:
1563+
if name.startswith('example'):
1564+
modfile = moddir / name
1565+
modfile.unlink()
1566+
except UnicodeDecodeError as e:
1567+
log.log(f"Error processing {item.name}: {e}")

ietf/utils/management/commands/periodic_tasks.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,16 @@ def create_default_tasks(self):
273273
),
274274
)
275275

276+
PeriodicTask.objects.get_or_create(
277+
name="Run Yang model checks",
278+
task="ietf.submit.tasks.run_yang_model_checks_task",
279+
defaults=dict(
280+
enabled=False,
281+
crontab=self.crontabs["daily"],
282+
description="Re-run Yang model checks on all active drafts",
283+
),
284+
)
285+
276286
def show_tasks(self):
277287
for label, crontab in self.crontabs.items():
278288
tasks = PeriodicTask.objects.filter(crontab=crontab).order_by(

0 commit comments

Comments
 (0)