Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | |
| 3 | import sys |
| 4 | import os |
Nathan Skrzypczak | 9ad39c0 | 2021-08-19 11:38:06 +0200 | [diff] [blame] | 5 | import os.path |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 6 | import ipaddress |
| 7 | import yaml |
| 8 | from pprint import pprint |
| 9 | import re |
Ole Troan | f3aebda | 2020-01-03 16:37:27 +0100 | [diff] [blame] | 10 | from jsonschema import validate, exceptions |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 11 | import argparse |
| 12 | from subprocess import run, PIPE |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 13 | from io import StringIO |
Nathan Skrzypczak | 9ad39c0 | 2021-08-19 11:38:06 +0200 | [diff] [blame] | 14 | import urllib.parse |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 15 | |
| 16 | # VPP feature JSON schema |
| 17 | schema = { |
| 18 | "$schema": "http://json-schema.org/schema#", |
| 19 | "type": "object", |
| 20 | "properties": { |
| 21 | "name": {"type": "string"}, |
Paul Vinciguerra | ea1a651 | 2019-11-01 02:34:32 -0400 | [diff] [blame] | 22 | "description": {"type": "string"}, |
Ole Troan | e774a8b | 2020-01-02 22:32:57 +0100 | [diff] [blame] | 23 | "maintainer": {"$ref": "#/definitions/maintainers"}, |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 24 | "state": {"type": "string", |
Ole Troan | e774a8b | 2020-01-02 22:32:57 +0100 | [diff] [blame] | 25 | "enum": ["production", "experimental", "development"]}, |
Paul Vinciguerra | ea1a651 | 2019-11-01 02:34:32 -0400 | [diff] [blame] | 26 | "features": {"$ref": "#/definitions/features"}, |
| 27 | "missing": {"$ref": "#/definitions/features"}, |
| 28 | "properties": {"type": "array", |
| 29 | "items": {"type": "string", |
| 30 | "enum": ["API", "CLI", "STATS", |
| 31 | "MULTITHREAD"]}, |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 32 | }, |
| 33 | }, |
| 34 | "additionalProperties": False, |
| 35 | "definitions": { |
Ole Troan | e774a8b | 2020-01-02 22:32:57 +0100 | [diff] [blame] | 36 | "maintainers": { |
| 37 | "anyof": [{ |
| 38 | "type": "array", |
| 39 | "items": {"type": "string"}, |
| 40 | "minItems": 1, |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 41 | }, |
Ole Troan | e774a8b | 2020-01-02 22:32:57 +0100 | [diff] [blame] | 42 | {"type": "string"}], |
| 43 | }, |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 44 | "featureobject": { |
| 45 | "type": "object", |
| 46 | "patternProperties": { |
Paul Vinciguerra | ea1a651 | 2019-11-01 02:34:32 -0400 | [diff] [blame] | 47 | "^.*$": {"$ref": "#/definitions/features"}, |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 48 | }, |
| 49 | }, |
| 50 | "features": { |
| 51 | "type": "array", |
Paul Vinciguerra | ea1a651 | 2019-11-01 02:34:32 -0400 | [diff] [blame] | 52 | "items": {"anyOf": [{"$ref": "#/definitions/featureobject"}, |
| 53 | {"type": "string"}, |
| 54 | ]}, |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 55 | "minItems": 1, |
| 56 | }, |
| 57 | }, |
| 58 | } |
| 59 | |
Nathan Skrzypczak | 9ad39c0 | 2021-08-19 11:38:06 +0200 | [diff] [blame] | 60 | DEFAULT_REPO_LINK = "https://github.com/FDio/vpp/blob/master/" |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 61 | |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 62 | def filelist_from_git_status(): |
| 63 | filelist = [] |
Dave Barach | 35ccd26 | 2020-06-17 08:05:37 -0400 | [diff] [blame] | 64 | git_status = 'git status --porcelain */FEATURE*.yaml' |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 65 | rv = run(git_status.split(), stdout=PIPE, stderr=PIPE) |
| 66 | if rv.returncode != 0: |
| 67 | sys.exit(rv.returncode) |
| 68 | |
| 69 | for l in rv.stdout.decode('ascii').split('\n'): |
| 70 | if len(l): |
| 71 | filelist.append(l.split()[1]) |
| 72 | return filelist |
| 73 | |
Paul Vinciguerra | ea1a651 | 2019-11-01 02:34:32 -0400 | [diff] [blame] | 74 | |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 75 | def filelist_from_git_ls(): |
| 76 | filelist = [] |
Dave Barach | 35ccd26 | 2020-06-17 08:05:37 -0400 | [diff] [blame] | 77 | git_ls = 'git ls-files :(top)*/FEATURE*.yaml' |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 78 | rv = run(git_ls.split(), stdout=PIPE, stderr=PIPE) |
| 79 | if rv.returncode != 0: |
| 80 | sys.exit(rv.returncode) |
| 81 | |
| 82 | for l in rv.stdout.decode('ascii').split('\n'): |
| 83 | if len(l): |
| 84 | filelist.append(l) |
| 85 | return filelist |
| 86 | |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 87 | def version_from_git(): |
| 88 | git_describe = 'git describe' |
| 89 | rv = run(git_describe.split(), stdout=PIPE, stderr=PIPE) |
| 90 | if rv.returncode != 0: |
| 91 | sys.exit(rv.returncode) |
| 92 | return rv.stdout.decode('ascii').split('\n')[0] |
Paul Vinciguerra | ea1a651 | 2019-11-01 02:34:32 -0400 | [diff] [blame] | 93 | |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 94 | class MarkDown(): |
| 95 | _dispatch = {} |
| 96 | |
| 97 | def __init__(self, stream): |
| 98 | self.stream = stream |
| 99 | self.toc = [] |
| 100 | |
| 101 | def print_maintainer(self, o): |
| 102 | write = self.stream.write |
| 103 | if type(o) is list: |
| 104 | write('Maintainers: ' + |
Dave Wallace | a079844 | 2020-09-23 11:38:25 -0400 | [diff] [blame] | 105 | ', '.join('{m}'.format(m=m) for m in |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 106 | o) + ' \n') |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 107 | else: |
Dave Wallace | a079844 | 2020-09-23 11:38:25 -0400 | [diff] [blame] | 108 | write('Maintainer: {o} \n'.format(o=o)) |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 109 | |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 110 | _dispatch['maintainer'] = print_maintainer |
Paul Vinciguerra | ea1a651 | 2019-11-01 02:34:32 -0400 | [diff] [blame] | 111 | |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 112 | def print_features(self, o, indent=0): |
| 113 | write = self.stream.write |
| 114 | for f in o: |
| 115 | indentstr = ' ' * indent |
| 116 | if type(f) is dict: |
| 117 | for k, v in f.items(): |
Dave Wallace | a079844 | 2020-09-23 11:38:25 -0400 | [diff] [blame] | 118 | write('{indentstr}- {k}\n'.format(indentstr=indentstr, k=k)) |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 119 | self.print_features(v, indent + 2) |
| 120 | else: |
Dave Wallace | a079844 | 2020-09-23 11:38:25 -0400 | [diff] [blame] | 121 | write('{indentstr}- {f}\n'.format(indentstr=indentstr, f=f)) |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 122 | write('\n') |
| 123 | _dispatch['features'] = print_features |
| 124 | |
| 125 | def print_markdown_header(self, o): |
| 126 | write = self.stream.write |
Dave Wallace | a079844 | 2020-09-23 11:38:25 -0400 | [diff] [blame] | 127 | write('## {o}\n'.format(o=o)) |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 128 | _dispatch['markdown_header'] = print_markdown_header |
| 129 | |
| 130 | def print_name(self, o): |
| 131 | write = self.stream.write |
Dave Wallace | a079844 | 2020-09-23 11:38:25 -0400 | [diff] [blame] | 132 | write('### {o}\n'.format(o=o)) |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 133 | self.toc.append(o) |
| 134 | _dispatch['name'] = print_name |
| 135 | |
| 136 | def print_description(self, o): |
| 137 | write = self.stream.write |
Dave Wallace | a079844 | 2020-09-23 11:38:25 -0400 | [diff] [blame] | 138 | write('\n{o}\n\n'.format(o=o)) |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 139 | _dispatch['description'] = print_description |
| 140 | |
| 141 | def print_state(self, o): |
| 142 | write = self.stream.write |
Dave Wallace | a079844 | 2020-09-23 11:38:25 -0400 | [diff] [blame] | 143 | write('Feature maturity level: {o} \n'.format(o=o)) |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 144 | _dispatch['state'] = print_state |
| 145 | |
| 146 | def print_properties(self, o): |
| 147 | write = self.stream.write |
Dave Wallace | a079844 | 2020-09-23 11:38:25 -0400 | [diff] [blame] | 148 | write('Supports: {s} \n'.format(s=" ".join(o))) |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 149 | _dispatch['properties'] = print_properties |
| 150 | |
| 151 | def print_missing(self, o): |
| 152 | write = self.stream.write |
| 153 | write('\nNot yet implemented: \n') |
| 154 | self.print_features(o) |
| 155 | _dispatch['missing'] = print_missing |
| 156 | |
| 157 | def print_code(self, o): |
| 158 | write = self.stream.write |
Dave Wallace | a079844 | 2020-09-23 11:38:25 -0400 | [diff] [blame] | 159 | write('Source Code: [{o}]({o}) \n'.format(o=o)) |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 160 | _dispatch['code'] = print_code |
| 161 | |
| 162 | def print(self, t, o): |
| 163 | write = self.stream.write |
| 164 | if t in self._dispatch: |
| 165 | self._dispatch[t](self, o,) |
Ole Troan | e774a8b | 2020-01-02 22:32:57 +0100 | [diff] [blame] | 166 | else: |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 167 | write('NOT IMPLEMENTED: {t}\n') |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 168 | |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 169 | def output_toc(toc, stream): |
| 170 | write = stream.write |
Nathan Skrzypczak | 9ad39c0 | 2021-08-19 11:38:06 +0200 | [diff] [blame] | 171 | write('# VPP Supported Features\n') |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 172 | |
| 173 | for t in toc: |
| 174 | ref = t.lower().replace(' ', '-') |
Dave Wallace | a079844 | 2020-09-23 11:38:25 -0400 | [diff] [blame] | 175 | write('[{t}](#{ref}) \n'.format(t=t, ref=ref)) |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 176 | |
| 177 | def featuresort(k): |
| 178 | return k[1]['name'] |
| 179 | |
| 180 | def featurelistsort(k): |
| 181 | orderedfields = { |
| 182 | 'name': 0, |
| 183 | 'maintainer': 1, |
| 184 | 'description': 2, |
| 185 | 'features': 3, |
| 186 | 'state': 4, |
| 187 | 'properties': 5, |
| 188 | 'missing': 6, |
| 189 | 'code': 7, |
| 190 | } |
| 191 | return orderedfields[k[0]] |
| 192 | |
Nathan Skrzypczak | 9ad39c0 | 2021-08-19 11:38:06 +0200 | [diff] [blame] | 193 | def output_markdown(features, fields, notfields, repository_url): |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 194 | stream = StringIO() |
| 195 | m = MarkDown(stream) |
| 196 | m.print('markdown_header', 'Feature Details:') |
| 197 | for path, featuredef in sorted(features.items(), key=featuresort): |
Nathan Skrzypczak | 9ad39c0 | 2021-08-19 11:38:06 +0200 | [diff] [blame] | 198 | codeurl = urllib.parse.urljoin(repository_url, os.path.dirname(path)) |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 199 | featuredef['code'] = codeurl |
| 200 | for k, v in sorted(featuredef.items(), key=featurelistsort): |
| 201 | if notfields: |
| 202 | if k not in notfields: |
| 203 | m.print(k, v) |
| 204 | elif fields: |
| 205 | if k in fields: |
| 206 | m.print(k, v) |
| 207 | else: |
| 208 | m.print(k, v) |
| 209 | |
| 210 | tocstream = StringIO() |
| 211 | output_toc(m.toc, tocstream) |
| 212 | return tocstream, stream |
Paul Vinciguerra | ea1a651 | 2019-11-01 02:34:32 -0400 | [diff] [blame] | 213 | |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 214 | def main(): |
| 215 | parser = argparse.ArgumentParser(description='VPP Feature List.') |
| 216 | parser.add_argument('--validate', dest='validate', action='store_true', |
| 217 | help='validate the FEATURE.yaml file') |
Nathan Skrzypczak | 9ad39c0 | 2021-08-19 11:38:06 +0200 | [diff] [blame] | 218 | parser.add_argument("--repolink", metavar="repolink", default=DEFAULT_REPO_LINK, |
| 219 | help="Link to public repository [%s]" % |
| 220 | DEFAULT_REPO_LINK) |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 221 | parser.add_argument('--git-status', dest='git_status', action='store_true', |
| 222 | help='Get filelist from git status') |
| 223 | parser.add_argument('--all', dest='all', action='store_true', |
| 224 | help='Validate all files in repository') |
| 225 | parser.add_argument('--markdown', dest='markdown', action='store_true', |
| 226 | help='Output feature table in markdown') |
| 227 | parser.add_argument('infile', nargs='?', type=argparse.FileType('r'), |
| 228 | default=sys.stdin) |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 229 | group = parser.add_mutually_exclusive_group() |
| 230 | group.add_argument('--include', help='List of fields to include') |
| 231 | group.add_argument('--exclude', help='List of fields to exclude') |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 232 | args = parser.parse_args() |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 233 | features = {} |
| 234 | |
| 235 | if args.git_status: |
| 236 | filelist = filelist_from_git_status() |
| 237 | elif args.all: |
| 238 | filelist = filelist_from_git_ls() |
| 239 | else: |
| 240 | filelist = args.infile |
| 241 | |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 242 | if args.include: |
| 243 | fields = args.include.split(',') |
| 244 | else: |
| 245 | fields = [] |
| 246 | if args.exclude: |
| 247 | notfields = args.exclude.split(',') |
| 248 | else: |
| 249 | notfields = [] |
| 250 | |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 251 | for featurefile in filelist: |
| 252 | featurefile = featurefile.rstrip() |
| 253 | |
| 254 | # Load configuration file |
Paul Vinciguerra | 36686f4 | 2020-05-05 11:46:26 -0400 | [diff] [blame] | 255 | with open(featurefile, encoding='utf-8') as f: |
Paul Vinciguerra | ea1a651 | 2019-11-01 02:34:32 -0400 | [diff] [blame] | 256 | cfg = yaml.load(f, Loader=yaml.SafeLoader) |
Ole Troan | f3aebda | 2020-01-03 16:37:27 +0100 | [diff] [blame] | 257 | try: |
| 258 | validate(instance=cfg, schema=schema) |
| 259 | except exceptions.ValidationError: |
Dave Wallace | a079844 | 2020-09-23 11:38:25 -0400 | [diff] [blame] | 260 | print('File does not validate: {featurefile}' \ |
| 261 | .format(featurefile=featurefile), file=sys.stderr) |
Ole Troan | f3aebda | 2020-01-03 16:37:27 +0100 | [diff] [blame] | 262 | raise |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 263 | features[featurefile] = cfg |
| 264 | |
| 265 | if args.markdown: |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 266 | stream = StringIO() |
Nathan Skrzypczak | 9ad39c0 | 2021-08-19 11:38:06 +0200 | [diff] [blame] | 267 | tocstream, stream = output_markdown(features, fields, notfields, args.repolink) |
Ole Troan | dbbff85 | 2020-01-08 12:37:55 +0100 | [diff] [blame] | 268 | print(tocstream.getvalue()) |
| 269 | print(stream.getvalue()) |
| 270 | stream.close() |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 271 | |
Paul Vinciguerra | ea1a651 | 2019-11-01 02:34:32 -0400 | [diff] [blame] | 272 | |
Ole Troan | 6a3064f | 2019-05-14 13:24:10 +0200 | [diff] [blame] | 273 | if __name__ == '__main__': |
| 274 | main() |