| #!/usr/bin/env python3 |
| |
| import sys |
| import os |
| import os.path |
| import ipaddress |
| import yaml |
| from pprint import pprint |
| import re |
| from jsonschema import validate, exceptions |
| import argparse |
| from subprocess import run, PIPE |
| from io import StringIO |
| import urllib.parse |
| |
| # VPP feature JSON schema |
| schema = { |
| "$schema": "http://json-schema.org/schema#", |
| "type": "object", |
| "properties": { |
| "name": {"type": "string"}, |
| "description": {"type": "string"}, |
| "maintainer": {"$ref": "#/definitions/maintainers"}, |
| "state": {"type": "string", |
| "enum": ["production", "experimental", "development"]}, |
| "features": {"$ref": "#/definitions/features"}, |
| "missing": {"$ref": "#/definitions/features"}, |
| "properties": {"type": "array", |
| "items": {"type": "string", |
| "enum": ["API", "CLI", "STATS", |
| "MULTITHREAD"]}, |
| }, |
| }, |
| "additionalProperties": False, |
| "definitions": { |
| "maintainers": { |
| "anyof": [{ |
| "type": "array", |
| "items": {"type": "string"}, |
| "minItems": 1, |
| }, |
| {"type": "string"}], |
| }, |
| "featureobject": { |
| "type": "object", |
| "patternProperties": { |
| "^.*$": {"$ref": "#/definitions/features"}, |
| }, |
| }, |
| "features": { |
| "type": "array", |
| "items": {"anyOf": [{"$ref": "#/definitions/featureobject"}, |
| {"type": "string"}, |
| ]}, |
| "minItems": 1, |
| }, |
| }, |
| } |
| |
| DEFAULT_REPO_LINK = "https://github.com/FDio/vpp/blob/master/" |
| |
| def filelist_from_git_status(): |
| filelist = [] |
| git_status = 'git status --porcelain */FEATURE*.yaml' |
| rv = run(git_status.split(), stdout=PIPE, stderr=PIPE) |
| if rv.returncode != 0: |
| sys.exit(rv.returncode) |
| |
| for l in rv.stdout.decode('ascii').split('\n'): |
| if len(l): |
| filelist.append(l.split()[1]) |
| return filelist |
| |
| |
| def filelist_from_git_ls(): |
| filelist = [] |
| git_ls = 'git ls-files :(top)*/FEATURE*.yaml' |
| rv = run(git_ls.split(), stdout=PIPE, stderr=PIPE) |
| if rv.returncode != 0: |
| sys.exit(rv.returncode) |
| |
| for l in rv.stdout.decode('ascii').split('\n'): |
| if len(l): |
| filelist.append(l) |
| return filelist |
| |
| def version_from_git(): |
| git_describe = 'git describe' |
| rv = run(git_describe.split(), stdout=PIPE, stderr=PIPE) |
| if rv.returncode != 0: |
| sys.exit(rv.returncode) |
| return rv.stdout.decode('ascii').split('\n')[0] |
| |
| class MarkDown(): |
| _dispatch = {} |
| |
| def __init__(self, stream): |
| self.stream = stream |
| self.toc = [] |
| |
| def print_maintainer(self, o): |
| write = self.stream.write |
| if type(o) is list: |
| write('Maintainers: ' + |
| ', '.join('{m}'.format(m=m) for m in |
| o) + ' \n') |
| else: |
| write('Maintainer: {o} \n'.format(o=o)) |
| |
| _dispatch['maintainer'] = print_maintainer |
| |
| def print_features(self, o, indent=0): |
| write = self.stream.write |
| for f in o: |
| indentstr = ' ' * indent |
| if type(f) is dict: |
| for k, v in f.items(): |
| write('{indentstr}- {k}\n'.format(indentstr=indentstr, k=k)) |
| self.print_features(v, indent + 2) |
| else: |
| write('{indentstr}- {f}\n'.format(indentstr=indentstr, f=f)) |
| write('\n') |
| _dispatch['features'] = print_features |
| |
| def print_markdown_header(self, o): |
| write = self.stream.write |
| write('## {o}\n'.format(o=o)) |
| _dispatch['markdown_header'] = print_markdown_header |
| |
| def print_name(self, o): |
| write = self.stream.write |
| write('### {o}\n'.format(o=o)) |
| self.toc.append(o) |
| _dispatch['name'] = print_name |
| |
| def print_description(self, o): |
| write = self.stream.write |
| write('\n{o}\n\n'.format(o=o)) |
| _dispatch['description'] = print_description |
| |
| def print_state(self, o): |
| write = self.stream.write |
| write('Feature maturity level: {o} \n'.format(o=o)) |
| _dispatch['state'] = print_state |
| |
| def print_properties(self, o): |
| write = self.stream.write |
| write('Supports: {s} \n'.format(s=" ".join(o))) |
| _dispatch['properties'] = print_properties |
| |
| def print_missing(self, o): |
| write = self.stream.write |
| write('\nNot yet implemented: \n') |
| self.print_features(o) |
| _dispatch['missing'] = print_missing |
| |
| def print_code(self, o): |
| write = self.stream.write |
| write('Source Code: [{o}]({o}) \n'.format(o=o)) |
| _dispatch['code'] = print_code |
| |
| def print(self, t, o): |
| write = self.stream.write |
| if t in self._dispatch: |
| self._dispatch[t](self, o,) |
| else: |
| write('NOT IMPLEMENTED: {t}\n') |
| |
| def output_toc(toc, stream): |
| write = stream.write |
| write('# VPP Supported Features\n') |
| |
| for t in toc: |
| ref = t.lower().replace(' ', '-') |
| write('[{t}](#{ref}) \n'.format(t=t, ref=ref)) |
| |
| def featuresort(k): |
| return k[1]['name'] |
| |
| def featurelistsort(k): |
| orderedfields = { |
| 'name': 0, |
| 'maintainer': 1, |
| 'description': 2, |
| 'features': 3, |
| 'state': 4, |
| 'properties': 5, |
| 'missing': 6, |
| 'code': 7, |
| } |
| return orderedfields[k[0]] |
| |
| def output_markdown(features, fields, notfields, repository_url): |
| stream = StringIO() |
| m = MarkDown(stream) |
| m.print('markdown_header', 'Feature Details:') |
| for path, featuredef in sorted(features.items(), key=featuresort): |
| codeurl = urllib.parse.urljoin(repository_url, os.path.dirname(path)) |
| featuredef['code'] = codeurl |
| for k, v in sorted(featuredef.items(), key=featurelistsort): |
| if notfields: |
| if k not in notfields: |
| m.print(k, v) |
| elif fields: |
| if k in fields: |
| m.print(k, v) |
| else: |
| m.print(k, v) |
| |
| tocstream = StringIO() |
| output_toc(m.toc, tocstream) |
| return tocstream, stream |
| |
| def main(): |
| parser = argparse.ArgumentParser(description='VPP Feature List.') |
| parser.add_argument('--validate', dest='validate', action='store_true', |
| help='validate the FEATURE.yaml file') |
| parser.add_argument("--repolink", metavar="repolink", default=DEFAULT_REPO_LINK, |
| help="Link to public repository [%s]" % |
| DEFAULT_REPO_LINK) |
| parser.add_argument('--git-status', dest='git_status', action='store_true', |
| help='Get filelist from git status') |
| parser.add_argument('--all', dest='all', action='store_true', |
| help='Validate all files in repository') |
| parser.add_argument('--markdown', dest='markdown', action='store_true', |
| help='Output feature table in markdown') |
| parser.add_argument('infile', nargs='?', type=argparse.FileType('r'), |
| default=sys.stdin) |
| group = parser.add_mutually_exclusive_group() |
| group.add_argument('--include', help='List of fields to include') |
| group.add_argument('--exclude', help='List of fields to exclude') |
| args = parser.parse_args() |
| features = {} |
| |
| if args.git_status: |
| filelist = filelist_from_git_status() |
| elif args.all: |
| filelist = filelist_from_git_ls() |
| else: |
| filelist = args.infile |
| |
| if args.include: |
| fields = args.include.split(',') |
| else: |
| fields = [] |
| if args.exclude: |
| notfields = args.exclude.split(',') |
| else: |
| notfields = [] |
| |
| for featurefile in filelist: |
| featurefile = featurefile.rstrip() |
| |
| # Load configuration file |
| with open(featurefile, encoding='utf-8') as f: |
| cfg = yaml.load(f, Loader=yaml.SafeLoader) |
| try: |
| validate(instance=cfg, schema=schema) |
| except exceptions.ValidationError: |
| print('File does not validate: {featurefile}' \ |
| .format(featurefile=featurefile), file=sys.stderr) |
| raise |
| features[featurefile] = cfg |
| |
| if args.markdown: |
| stream = StringIO() |
| tocstream, stream = output_markdown(features, fields, notfields, args.repolink) |
| print(tocstream.getvalue()) |
| print(stream.getvalue()) |
| stream.close() |
| |
| |
| if __name__ == '__main__': |
| main() |