Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add yanglint feature #193

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions doc/FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ Turn internet-draft source into text and HTML. This supports XML files using
[kramdown-rfc2629](https://github.com/cabo/kramdown-rfc2629) or
[mmark](https://github.com/miekg/mmark)

```sh
$ make yanglint
```

Check YANG modules and examples for errors and warnings (see [YANG](YANG.md)). Also runs automatically during `make` if the `VALIDATE_YANG` environment variable is set.

```sh
$ make diff
```
Expand Down
26 changes: 26 additions & 0 deletions doc/SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,32 @@ and find a directory that is on the path where you can install `mmark`. For
these, I set `GOPATH=~/gocode`.


## pyang

[`pyang`](https://github.com/mbj4668/pyang) is needed for markdown that uses `YANG-TREE <module.yang>` to import an external yang file.

```sh
$ pip install pyang
```


## yanglint / libyang

[`yanglint`](https://github.com/CESNET/libyang/tree/master/tools/lint) is part of the [`libyang`](https://github.com/CESNET/libyang) package. It's required only if you're validating YANG modules with `make yanglint` or with the `VALIDATE_YANG=1` environment variable during make.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrap text to 80 columns.


In late 2019, the libyang package is unfortunately not yet widely available as an rpm, deb, brew, etc. package in stable distros like for example the latest LTS, Bionic Beaver. So if you're using this feature, it may be necessary to install from source. This requires `cmake` and `libpcre3-dev`:

```sh
git clone https://github.com/CESNET/libyang.git
mkdir libyang/build
pushd libyang/build
cmake -DCMAKE_INSTALL_PREFIX=/ ..
make
make install
popd
```


## Other tools

Some other helpful tools are listed in `config.mk`.
49 changes: 49 additions & 0 deletions doc/YANG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Working with YANG [RFC7950](https://tools.ietf.org/rfc7950)

In the bad old days, YANG modules were hand-copied into markdown files, but
this became untenable as YANG started getting more usage.

There are 3 YANG features that will be picked up during make, and which
will have enhanced error checking when the VALIDATE_YANG environment
variable is set to 1 or when running `make yanglint`:

* `YANG-MODULE <ietf-example.yang>`: inserts the text of the .yang file
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first level of bullets should be flush with the left column.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not clear whether this is a feature of the markdown processor or whether make yanglint does this. If it is make yanglint, then that won't be integrated into the build process properly.

before building the markdown. Usually inside a diagram. Inserts
\<CODE BEGINS\> and \<CODE ENDS\> tags in accordance with
[Section 3.2 of RFC 8407](https://tools.ietf.org/html/rfc8407#section-3.2)

* when VALIDATE_YANG=1 or during yanglint, also performs a pyang lint check for IETF rules.

* `YANG-TREE <ietf-example.yang>`: inserts the tree diagram of the .yang
file (as generated by pyang) before building the markdown, in
accordance with
[Section 3.4 of RFC 8407](https://tools.ietf.org/html/rfc8407#section-3.4)

* `YANG-DATA <ietf-example.yang> <example-data.json>`: inserts the text
of the .json file before building the markdown.

* when VALIDATE_YANG=1 or when running `make yanglint`, also performs a yanglint check to ensure that
the example-data.json validates against the model in ietf-example.yang.
(yanglint is part of the [libyang](https://github.com/CESNET/libyang)
project)

Note that the lint checks occur only for YANG modules and examples that
are imported with these commands from files external to the markdown.
If you have YANG examples or modules embedded in the markdown, they are
not examined by this feature.

## Motivation

It's useful to maintain separate YANG module files, because these can be
dropped directly into various tools that make use of them. By incorporating
the YANG module file into the I-D source markdown by reference instead of by
copy/paste, it becomes easier and more robust to ensure the actual .yang
files and the text of the draft don't accidentally diverge.

Likewise, examples are incredibly useful for understanding how the yang
model is best used, and incredibly easy to diverge in the course of
refactoring the YANG model.

It's hoped that by integrating support for external YANG files, the
quality of drafts that include YANG modules will improve with reduced
effort for authors.
24 changes: 20 additions & 4 deletions main.mk
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ include $(LIBDIR)/ghpages.mk
include $(LIBDIR)/issues.mk
include $(LIBDIR)/upload.mk
include $(LIBDIR)/update.mk
include $(LIBDIR)/yanglint.mk

## Basic Targets
.PHONY: txt html pdf
Expand All @@ -32,16 +33,30 @@ REMOVE_LATEST =
endif
export XML_RESOURCE_ORG_PREFIX

# x.md.yangdeps files created by yang-inject.sh
-include $(addsuffix .yangdeps,$(wildcard *.md))

%.xml: %.md
@h=$$(head -1 $< | cut -c 1-4 -); set -o pipefail; \
if [ "$${h:0:1}" = $$'\ufeff' ]; then echo 'warning: BOM in $<' 1>&2; h="$${h:1:3}"; \
else h="$${h:0:3}"; fi; \
if grep -q -E '^\s*YANG-(MODULE|DATA|TREE)' $< ; then \
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The different markdown formats have provisions for including files directly and you should use that instead of this ad-hoc enhancement.

In that case, your linting step can grep for the different inclusion methods, or look for modules that are directly defines and extract those into temporary files. Validating the resulting files can then be a separate step.

if [ "$(VALIDATE_YANG)" = "1" ]; then \
if ! $(LIBDIR)/yang-check.sh $< ; then \
echo "$(LIBDIR)/yang-check.sh $< failed" ; \
exit 1 ; \
fi ; \
fi ;\
if $(LIBDIR)/yang-inject.sh $< ; then \
f="$<.withyang"; \
else echo "$(LIBDIR)/yang-inject.sh $< failed" ; exit 1 ; fi ; \
else f="$<" ; fi; \
if [ "$$h" = '---' ]; then \
echo '$(subst ','"'"',cat $< $(MD_PREPROCESSOR) $(REMOVE_LATEST) | $(kramdown-rfc2629) > $@)'; \
cat $< $(MD_PREPROCESSOR) $(REMOVE_LATEST) | $(kramdown-rfc2629) > $@; \
echo '$(subst ','"'"',cat $$f $(MD_PREPROCESSOR) $(REMOVE_LATEST) | $(kramdown-rfc2629) > $@)'; \
cat $$f $(MD_PREPROCESSOR) $(REMOVE_LATEST) | $(kramdown-rfc2629) > $@; \
elif [ "$$h" = '%%%' ]; then \
echo '$(subst ','"'"',cat $< $(MD_PREPROCESSOR) $(REMOVE_LATEST) | $(mmark) -xml2 -page > $@)'; \
cat $< $(MD_PREPROCESSOR) $(REMOVE_LATEST) | $(mmark) -xml2 -page > $@; \
echo '$(subst ','"'"',cat $$f $(MD_PREPROCESSOR) $(REMOVE_LATEST) | $(mmark) -xml2 -page > $@)'; \
cat $$f $(MD_PREPROCESSOR) $(REMOVE_LATEST) | $(mmark) -xml2 -page > $@; \
else \
! echo "Unable to detect '%%%' or '---' in markdown file" 1>&2; \
fi
Expand Down Expand Up @@ -183,6 +198,7 @@ COMMA := ,
clean::
-rm -f .tags $(targets_file) issues.json \
$(addsuffix .{txt$(COMMA)html$(COMMA)pdf},$(drafts)) index.html \
$(addsuffix .{md.withyang$(COMMA)md.yangdeps},$(drafts)) \
$(addsuffix -[0-9][0-9].{xml$(COMMA)md$(COMMA)org$(COMMA)txt$(COMMA)html$(COMMA)pdf},$(drafts)) \
$(filter-out $(join $(drafts),$(draft_types)),$(addsuffix .xml,$(drafts))) \
$(uploads) $(draft_diffs)
4 changes: 4 additions & 0 deletions template/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
.tags
*~
*.swp
.*.sw?
*.withyang
*.yangdeps
modules/
/*-[0-9][0-9].xml
.refcache
.targets.mk
Expand Down
177 changes: 177 additions & 0 deletions yang-check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
#!/usr/bin/env bash

input_md=$1

if [ ! -f "$input_md" ] ; then
echo "usage: $0 <input.md> :"
echo " inserts yang into input.md+validates if VALIDATE_YANG=1"
echo " YANG-MODULE <mod.yang> (pyang lint)"
echo " YANG-TREE <mod.yang> (pyang lint)"
echo " YANG-DATA <mod.yang> <example.json> (yanglint validate json)"
echo " uses rsync to download imported modules to modules/ from iana"
echo "error: no input file: '$input_md'"
exit 1
fi

modules=$(grep YANG-MODULE $input_md | awk '{print $2;}' | tr "\n" " ")
tree_modules=$(grep YANG-TREE $input_md | awk '{print $2;}' | tr "\n" " ")
data_modules_str=$(grep YANG-DATA $input_md | awk '{print $2;}' | tr "\n" " ")

data_json_str="$(grep YANG-DATA $input_md | awk '{print $3;}' | tr "\n" " ")"
all_modules="$(echo "$modules $tree_modules $data_modules_str" | tr " " "\n" | sort | uniq | tr "\n" " " | sed -e 's/^ *//' | sed -e 's/ *$//')"

data_jsons=(); for d in $data_json_str; do data_jsons+=("$d"); done
data_modules=(); for d in $data_modules_str; do data_modules+=("$d"); done

#echo "input=$input_md, data='${#data_jsons[@]}: $data_jsons', mods='$all_modules'"

had_error=0

target_xml="$(echo ${input_md} | sed -e 's/.md$/.xml/')"
for ((i=0; i < ${#data_jsons[@]}; i++)); do
if [ ! -f "${data_jsons[i]}" ]; then
echo "no example data file ${data_jsons[i]}"
had_error=1
fi
done
for mod in ${all_modules}; do
if [ ! -f ${mod} ]; then
echo "no module file ${mod}"
had_error=1
fi
done

if [ "${had_error}" != "0" ]; then
echo "error: some files not found, exiting"
exit 1
fi

fatal=0
if ! which pyang 2>&1 > /dev/null ; then
echo "error in $0: pyang required; please install\n(pip install pyang)"
fatal=1
fi

if ! which yanglint 2>&1 > /dev/null ; then
echo "error in $0: yanglint required; please install libyang:"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could download and install yanglint itself if it was missing. That would help in CI.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems a bit rude and maybe requires privileges to add something in PATH. I guess maybe install into an environment variable location when it's missing and look for it there?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't suggesting that you put this in /usr/local or anything like that. But you can download and compile the tool and run that copy. You can see an example of that sort of process in https://github.com/w3c/push-api/blob/gh-pages/Makefile

echo "(install libyang-dev or equivalent if available, or build:)"
echo " git clone https://github.com/CESNET/libyang"
echo " mkdir libyang/build"
echo " pushd libyang/build"
echo " cmake .."
echo " make && sudo make install"
echo " popd"
echo " yanglint --help"
echo "if it can't load libyang.so, try something like:"
echo " cmake -DCMAKE_INSTALL_PREFIX=$$HOME/local-installs -DCMAKE_INSTALL_RPATH=$$HOME/local-installs/lib .."
fatal=1
fi

if [ "$fatal" != "0" ]; then
exit 1
fi

set -e

if [ ! -d modules/ ]; then
mkdir -p modules/
fi

before=$(ls modules/ | wc -l)
after=${before}
last=$((before-1))

echo "checking for module dependencies (downloading into modules/...)"
while [ "${last}" != "${after}" ]; do
last=${after}
for mod in ${all_modules}; do
missing=$( ( pyang --verbose --path modules:. ${mod} 2>&1 || true ) | \
grep "error: module" | \
sed -e 's/.*: error: module "\([^"]*\)" not found in search path.*/\1/')
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
sed -e 's/.*: error: module "\([^"]*\)" not found in search path.*/\1/')
sed -e 's/: error: module "\([^"]*\)" not found in search path/\1/')

.* is redundant without $ or ^.

if [ "$missing" != "" ]; then
if ! which rsync 2>&1 > /dev/null ; then
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation does not mention a dependency on rsync.

echo "error in $0: rsync required; please install"
fi

for mis in ${missing}; do
bash -e -x -c "rsync -cvz rsync.iana.org::assignments/yang-parameters/${mis}*.yang modules/"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This script forks a lot of bash subscripts unnecessarily. That will result in bad quoting. For instance, if $mis contains a space, this will fail badly. And the * is passed unquoted to rsync. That's probably OK in this specific case, but it's not a good practice generally. You can invoke rsync directly.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can use rsync directly, sure. I was doing this mainly so that when rsync errors, the call that caused it will be visible, but the quoting is a very good point and I think it should echo instead, thanks.

done
fi

# sample:
# err : Importing "ietf-routing-types" module into "ietf-dorms" failed.

# unfortunately, you can't distinguish between missing and erroring,
# so in particular when multiple local modules import each other but
# the bottom one is missing things, skip pulling local stuff, and
# instead re-check those. TBD: set them up as dependencies?

# TBD: maybe better to use yangcatalog.org to support I-D refs?
# see, e.g.:
# https://www.yangcatalog.org/yang-search/[email protected]
missing=$( ( yanglint -f json -D -V -p . -p modules ${mod} 2>&1 || true ) | \
grep "err : Importing " | \
sed -e 's/err : Importing "\([^"]*\)" module .* failed./\1/')
locals=""
while [ -f ${missing}.yang ]; do
already=0
for loc in ${locals}; do
if [ "${loc}" = "${missing}.yang" ]; then
already=1
fi
done
if [ "${already}" != "0" ]; then
break
fi
locals="${locals} ${missing}.yang"
missing=$( ( yanglint -f json -D -V -p . -p modules ${mod} 2>&1 || true ) | \
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like code repetition. Use a function perhaps.

grep "err : Importing " | \
sed -e 's/err : Importing "\([^"]*\)" module .* failed./\1/')
done
if [ "$missing" != "" ]; then
for mis in ${missing}; do
if [ -f "$mis.yang" ]; then
missing=$( ( yanglint -f json -D -V -p . -p modules ${mis}.yang 2>&1 || true ) | \
grep "err : Importing " | \
sed -e 's/err : Importing "\([^"]*\)" module .* failed./\1/')
break
fi
done

if ! which rsync 2>&1 > /dev/null ; then
echo "error in $0: rsync required; please install"
fi
for mis in ${missing}; do
bash -e -x -c "rsync -cvz rsync.iana.org::assignments/yang-parameters/${mis}*.yang modules/"
done
fi
done
after=$(ls modules/ | wc -l)
done

echo "checking modules..."
for mod in ${all_modules}; do
if ! bash -x -e -c "pyang -E --verbose --ietf --lint --max-line-length 72 --path modules:. ${mod}" ; then
had_error=1
fi
done

echo "checking examples..."
for ((i=0; i < ${#data_modules[@]} && i < ${#data_jsons[@]}; i++)); do
# TBD: ideally, yanglint would have a "treat warnings as errors" mode,
# which ideally would be used here.
# TBD: ideally, also [email protected] would not contain
# a redundant version that always reports this warning:
# warn: Module's revisions are not unique (2018-06-28).
bash -x -e -c "yanglint -f json -t data -D -s -V -p modules -p . -o /dev/null ${data_modules[i]} ${data_jsons[i]}" 2>&1 | grep -v "warn: Module's revisions are not unique (2018-06-28)"
if [ "${PIPESTATUS[0]}" != "0" ]; then
had_error=1
fi
done

if [ "${had_error}" != "0" ]; then
echo "encountered yang errors in ${input_md}"
exit 1
fi
echo "yang passed checks in ${input_md}"

Loading