Zack Williams | 6e87cbc | 2017-07-17 16:48:01 -0700 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | # defaultsdoc.py - documentation for ansible default vaules |
| 3 | |
| 4 | # Copyright 2017-present Open Networking Foundation |
| 5 | # |
| 6 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| 7 | # you may not use this file except in compliance with the License. |
| 8 | # You may obtain a copy of the License at |
| 9 | # |
| 10 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 11 | # |
| 12 | # Unless required by applicable law or agreed to in writing, software |
| 13 | # distributed under the License is distributed on an "AS IS" BASIS, |
| 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 15 | # See the License for the specific language governing permissions and |
| 16 | # limitations under the License. |
| 17 | |
| 18 | import argparse |
| 19 | import fnmatch |
| 20 | import jinja2 |
| 21 | import logging |
| 22 | import os |
| 23 | import pprint |
| 24 | import re |
| 25 | import sys |
| 26 | import xml.etree.ElementTree as ET |
| 27 | import yaml |
| 28 | import markedyaml |
| 29 | |
| 30 | # logging setup |
| 31 | sh = logging.StreamHandler(sys.stderr) |
| 32 | sh.setFormatter(logging.Formatter(logging.BASIC_FORMAT)) |
| 33 | |
| 34 | LOG = logging.getLogger("defaultsdoc.py") |
| 35 | LOG.addHandler(sh) |
| 36 | |
| 37 | # parse args |
| 38 | parser = argparse.ArgumentParser() |
| 39 | |
| 40 | parser.add_argument('-p', '--playbook_dir', default='../platform-install/', |
| 41 | action='append', required=False, |
| 42 | help="path to base playbook directory") |
| 43 | |
| 44 | parser.add_argument('-d', '--descriptions', default='scripts/descriptions.md', |
| 45 | action='store', required=False, |
| 46 | help="markdown file with descriptions") |
| 47 | |
| 48 | parser.add_argument('-t', '--template', default='scripts/defaults.md.j2', |
| 49 | action='store', required=False, |
| 50 | help="jinja2 template to fill with defaults") |
| 51 | |
| 52 | parser.add_argument('-o', '--output', default='defaults.md', |
| 53 | action='store', required=False, |
| 54 | help="output file") |
| 55 | |
| 56 | args = parser.parse_args() |
| 57 | |
| 58 | # find the branch we're on via the repo manifest |
| 59 | manifest_path = os.path.abspath("../../.repo/manifest.xml") |
| 60 | try: |
| 61 | tree = ET.parse(manifest_path) |
| 62 | manifest_xml = tree.getroot() |
| 63 | repo_default = manifest_xml.find('default') |
| 64 | repo_branch = repo_default.attrib['revision'] |
| 65 | except Exception: |
| 66 | LOG.exception("Error loading repo manifest") |
| 67 | sys.exit(1) |
| 68 | |
| 69 | role_defs = [] |
| 70 | profile_defs = [] |
| 71 | group_defs = [] |
| 72 | |
| 73 | # frontmatter section is any text at the top of the descriptions.md file, and |
| 74 | # comes before all other sections |
| 75 | def_docs = {'frontmatter':{'description':''}} |
| 76 | |
| 77 | # find all the files to be processed |
| 78 | for dirpath, dirnames, filenames in os.walk(args.playbook_dir): |
| 79 | basepath = re.sub(args.playbook_dir, '', dirpath) |
| 80 | for filename in filenames : |
| 81 | filepath = os.path.join(basepath, filename) |
| 82 | |
| 83 | if fnmatch.fnmatch(filepath, "roles/*/defaults/*.yml"): |
| 84 | role_defs.append(filepath) |
| 85 | |
| 86 | if fnmatch.fnmatch(filepath, "profile_manifests/*.yml"): |
| 87 | profile_defs.append(filepath) |
| 88 | |
| 89 | if fnmatch.fnmatch(filepath, "group_vars/*.yml"): |
| 90 | group_defs.append(filepath) |
| 91 | |
| 92 | |
| 93 | |
| 94 | for rd in role_defs: |
| 95 | rd_vars = {} |
| 96 | # trim slash so basename grabs the final directory name |
| 97 | rd_basedir = os.path.basename(args.playbook_dir[:-1]) |
| 98 | try: |
| 99 | rd_fullpath = os.path.abspath(os.path.join(args.playbook_dir, rd)) |
| 100 | rd_partialpath = os.path.join(rd_basedir, rd) |
| 101 | |
| 102 | # partial URL, without line nums |
| 103 | rd_url = "https://github.com/opencord/platform-install/tree/%s/%s" % (repo_branch, rd) |
| 104 | |
| 105 | |
| 106 | rd_fh= open(rd_fullpath, 'r') |
| 107 | |
| 108 | # markedloader is for line #'s |
| 109 | loader = markedyaml.MarkedLoader(rd_fh.read()) |
| 110 | marked_vars = loader.get_data() |
| 111 | |
| 112 | rd_fh.seek(0) # go to front of file |
| 113 | |
| 114 | # yaml.safe_load is for vars in a better format |
| 115 | rd_vars = yaml.safe_load(rd_fh) |
| 116 | |
| 117 | rd_fh.close() |
| 118 | |
| 119 | except yaml.YAMLError: |
| 120 | LOG.exception("Problem loading file: %s" % rd) |
| 121 | sys.exit(1) |
| 122 | |
| 123 | if rd_vars: |
| 124 | |
| 125 | for key, val in rd_vars.iteritems(): |
| 126 | |
| 127 | # build full URL to lines. Lines numbered from zero, so +1 on them to match github |
| 128 | if marked_vars[key].start_mark.line == marked_vars[key].end_mark.line: |
| 129 | full_url = "%s#L%d" % (rd_url, marked_vars[key].start_mark.line+1) |
| 130 | else: |
| 131 | full_url = "%s#L%d-L%d" % (rd_url, marked_vars[key].start_mark.line, marked_vars[key].end_mark.line) |
| 132 | |
| 133 | if key in def_docs: |
| 134 | if def_docs[key]['defval'] == val: |
| 135 | def_docs[key]['reflist'].append({'path':rd_partialpath, 'link':full_url}) |
| 136 | else: |
| 137 | LOG.error(" %s has different default > %s : %s" % (rd, key, val)) |
| 138 | else: |
| 139 | to_print = { str(key): val } |
| 140 | pp = yaml.dump(to_print, indent=4, allow_unicode=False, default_flow_style=False) |
| 141 | |
| 142 | def_docs[key] = { |
| 143 | 'defval': val, |
| 144 | 'defval_pp': pp, |
| 145 | 'description': "", |
| 146 | 'reflist': [{'path':rd_partialpath, 'link':full_url}], |
| 147 | } |
| 148 | |
| 149 | # read in descriptions file |
| 150 | descriptions = {} |
| 151 | with open(args.descriptions, 'r') as descfile: |
| 152 | desc_name = 'frontmatter' |
| 153 | desc_lines = '' |
| 154 | |
| 155 | for d_l in descfile: |
| 156 | # see if this is a header line at beginning of docs |
| 157 | desc_header = re.match(r"##\s+([\w_]+)", d_l) |
| 158 | |
| 159 | if desc_header: |
| 160 | # add previous description to dict |
| 161 | descriptions[desc_name] = desc_lines |
| 162 | |
| 163 | # set this as the next name, wipe out lines |
| 164 | desc_name = desc_header.group(1) |
| 165 | desc_lines = '' |
| 166 | else: |
| 167 | desc_lines += d_l |
| 168 | |
| 169 | descriptions[desc_name] = desc_lines |
| 170 | |
| 171 | # add descriptions to def_docs |
| 172 | for d_name, d_text in descriptions.iteritems(): |
| 173 | if d_name in def_docs: |
| 174 | def_docs[d_name]['description'] = d_text |
| 175 | else: |
| 176 | LOG.error("Description exists for '%s' but doesn't exist in defaults" % d_name) |
| 177 | |
| 178 | # check for missing descriptions |
| 179 | for key in sorted(def_docs): |
| 180 | if not def_docs[key]['description']: |
| 181 | LOG.error("No description found for '%s'" % key) |
| 182 | |
| 183 | # Add to template and write to output file |
| 184 | j2env = jinja2.Environment( |
| 185 | loader = jinja2.FileSystemLoader('.') |
| 186 | ) |
| 187 | |
| 188 | template = j2env.get_template(args.template) |
| 189 | |
| 190 | with open(args.output, 'w') as f: |
| 191 | f.write(template.render(def_docs=def_docs)) |