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