Zack Williams | 1b96a95 | 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 |
Zack Williams | 1b96a95 | 2017-07-17 16:48:01 -0700 | [diff] [blame] | 23 | import re |
| 24 | import sys |
| 25 | import xml.etree.ElementTree as ET |
| 26 | import yaml |
| 27 | import markedyaml |
| 28 | |
| 29 | # logging setup |
| 30 | sh = logging.StreamHandler(sys.stderr) |
| 31 | sh.setFormatter(logging.Formatter(logging.BASIC_FORMAT)) |
| 32 | |
| 33 | LOG = logging.getLogger("defaultsdoc.py") |
| 34 | LOG.addHandler(sh) |
| 35 | |
| 36 | # parse args |
| 37 | parser = argparse.ArgumentParser() |
| 38 | |
| 39 | parser.add_argument('-p', '--playbook_dir', default='../platform-install/', |
| 40 | action='append', required=False, |
| 41 | help="path to base playbook directory") |
| 42 | |
| 43 | parser.add_argument('-d', '--descriptions', default='scripts/descriptions.md', |
| 44 | action='store', required=False, |
| 45 | help="markdown file with descriptions") |
| 46 | |
| 47 | parser.add_argument('-t', '--template', default='scripts/defaults.md.j2', |
| 48 | action='store', required=False, |
| 49 | help="jinja2 template to fill with defaults") |
| 50 | |
| 51 | parser.add_argument('-o', '--output', default='defaults.md', |
| 52 | action='store', required=False, |
| 53 | help="output file") |
| 54 | |
| 55 | args = parser.parse_args() |
| 56 | |
| 57 | # find the branch we're on via the repo manifest |
Zack Williams | be0b2c7 | 2018-01-25 14:48:38 -0700 | [diff] [blame] | 58 | manifest_path = os.path.abspath("../../.repo/manifest.xml") |
Zack Williams | 1b96a95 | 2017-07-17 16:48:01 -0700 | [diff] [blame] | 59 | try: |
| 60 | tree = ET.parse(manifest_path) |
| 61 | manifest_xml = tree.getroot() |
| 62 | repo_default = manifest_xml.find('default') |
| 63 | repo_branch = repo_default.attrib['revision'] |
| 64 | except Exception: |
| 65 | LOG.exception("Error loading repo manifest") |
| 66 | sys.exit(1) |
| 67 | |
| 68 | role_defs = [] |
| 69 | profile_defs = [] |
| 70 | group_defs = [] |
Zack Williams | be0b2c7 | 2018-01-25 14:48:38 -0700 | [diff] [blame] | 71 | def_docs = {} |
Zack Williams | 1b96a95 | 2017-07-17 16:48:01 -0700 | [diff] [blame] | 72 | |
| 73 | # find all the files to be processed |
| 74 | for dirpath, dirnames, filenames in os.walk(args.playbook_dir): |
| 75 | basepath = re.sub(args.playbook_dir, '', dirpath) |
Zack Williams | be0b2c7 | 2018-01-25 14:48:38 -0700 | [diff] [blame] | 76 | for filename in filenames: |
Zack Williams | 1b96a95 | 2017-07-17 16:48:01 -0700 | [diff] [blame] | 77 | filepath = os.path.join(basepath, filename) |
| 78 | |
| 79 | if fnmatch.fnmatch(filepath, "roles/*/defaults/*.yml"): |
| 80 | role_defs.append(filepath) |
| 81 | |
| 82 | if fnmatch.fnmatch(filepath, "profile_manifests/*.yml"): |
| 83 | profile_defs.append(filepath) |
| 84 | |
| 85 | if fnmatch.fnmatch(filepath, "group_vars/*.yml"): |
| 86 | group_defs.append(filepath) |
| 87 | |
| 88 | |
Zack Williams | 1b96a95 | 2017-07-17 16:48:01 -0700 | [diff] [blame] | 89 | for rd in role_defs: |
| 90 | rd_vars = {} |
| 91 | # trim slash so basename grabs the final directory name |
| 92 | rd_basedir = os.path.basename(args.playbook_dir[:-1]) |
| 93 | try: |
| 94 | rd_fullpath = os.path.abspath(os.path.join(args.playbook_dir, rd)) |
| 95 | rd_partialpath = os.path.join(rd_basedir, rd) |
| 96 | |
| 97 | # partial URL, without line nums |
Zack Williams | be0b2c7 | 2018-01-25 14:48:38 -0700 | [diff] [blame] | 98 | rd_url = "https://github.com/opencord/platform-install/tree/%s/%s" % ( |
| 99 | repo_branch, rd) |
Zack Williams | 1b96a95 | 2017-07-17 16:48:01 -0700 | [diff] [blame] | 100 | |
Zack Williams | be0b2c7 | 2018-01-25 14:48:38 -0700 | [diff] [blame] | 101 | rd_fh = open(rd_fullpath, 'r') |
Zack Williams | 1b96a95 | 2017-07-17 16:48:01 -0700 | [diff] [blame] | 102 | |
| 103 | # markedloader is for line #'s |
| 104 | loader = markedyaml.MarkedLoader(rd_fh.read()) |
| 105 | marked_vars = loader.get_data() |
| 106 | |
| 107 | rd_fh.seek(0) # go to front of file |
| 108 | |
| 109 | # yaml.safe_load is for vars in a better format |
| 110 | rd_vars = yaml.safe_load(rd_fh) |
| 111 | |
| 112 | rd_fh.close() |
| 113 | |
| 114 | except yaml.YAMLError: |
| 115 | LOG.exception("Problem loading file: %s" % rd) |
| 116 | sys.exit(1) |
| 117 | |
| 118 | if rd_vars: |
| 119 | |
| 120 | for key, val in rd_vars.iteritems(): |
| 121 | |
Zack Williams | be0b2c7 | 2018-01-25 14:48:38 -0700 | [diff] [blame] | 122 | # build full URL to lines. Lines numbered from zero, so +1 on them |
| 123 | # to match github |
| 124 | if marked_vars[key].start_mark.line == marked_vars[ |
| 125 | key].end_mark.line: |
| 126 | full_url = "%s#L%d" % (rd_url, |
| 127 | marked_vars[key].start_mark.line + 1) |
| 128 | else: |
| 129 | full_url = "%s#L%d-L%d" % (rd_url, |
| 130 | marked_vars[key].start_mark.line, |
| 131 | marked_vars[key].end_mark.line) |
Zack Williams | 1b96a95 | 2017-07-17 16:48:01 -0700 | [diff] [blame] | 132 | |
Zack Williams | be0b2c7 | 2018-01-25 14:48:38 -0700 | [diff] [blame] | 133 | if key in def_docs: |
Zack Williams | 1b96a95 | 2017-07-17 16:48:01 -0700 | [diff] [blame] | 134 | if def_docs[key]['defval'] == val: |
Zack Williams | be0b2c7 | 2018-01-25 14:48:38 -0700 | [diff] [blame] | 135 | def_docs[key]['reflist'].append( |
| 136 | {'path': rd_partialpath, 'link': full_url}) |
Zack Williams | 1b96a95 | 2017-07-17 16:48:01 -0700 | [diff] [blame] | 137 | else: |
Zack Williams | be0b2c7 | 2018-01-25 14:48:38 -0700 | [diff] [blame] | 138 | LOG.error( |
| 139 | " %s has different default > %s : %s" % |
| 140 | (rd, key, val)) |
| 141 | else: |
| 142 | to_print = {str(key): val} |
| 143 | pp = yaml.dump( |
| 144 | to_print, |
| 145 | indent=4, |
| 146 | allow_unicode=False, |
| 147 | default_flow_style=False) |
Zack Williams | 1b96a95 | 2017-07-17 16:48:01 -0700 | [diff] [blame] | 148 | |
| 149 | def_docs[key] = { |
Zack Williams | be0b2c7 | 2018-01-25 14:48:38 -0700 | [diff] [blame] | 150 | 'defval': val, |
| 151 | 'defval_pp': pp, |
| 152 | 'description': "", |
| 153 | 'reflist': [{'path': rd_partialpath, 'link': full_url}], |
| 154 | } |
Zack Williams | 1b96a95 | 2017-07-17 16:48:01 -0700 | [diff] [blame] | 155 | |
| 156 | # read in descriptions file |
| 157 | descriptions = {} |
| 158 | with open(args.descriptions, 'r') as descfile: |
| 159 | desc_name = 'frontmatter' |
| 160 | desc_lines = '' |
| 161 | |
| 162 | for d_l in descfile: |
| 163 | # see if this is a header line at beginning of docs |
| 164 | desc_header = re.match(r"##\s+([\w_]+)", d_l) |
| 165 | |
| 166 | if desc_header: |
| 167 | # add previous description to dict |
Zack Williams | be0b2c7 | 2018-01-25 14:48:38 -0700 | [diff] [blame] | 168 | descriptions[desc_name] = desc_lines.strip() |
Zack Williams | 1b96a95 | 2017-07-17 16:48:01 -0700 | [diff] [blame] | 169 | |
| 170 | # set this as the next name, wipe out lines |
| 171 | desc_name = desc_header.group(1) |
| 172 | desc_lines = '' |
| 173 | else: |
| 174 | desc_lines += d_l |
| 175 | |
Zack Williams | be0b2c7 | 2018-01-25 14:48:38 -0700 | [diff] [blame] | 176 | descriptions[desc_name] = desc_lines.strip() |
| 177 | |
| 178 | # Get the frontmatter out of descriptions, and remove the header line |
| 179 | frontmatter = re.sub(r'^#.*\n\n', '', descriptions.pop('frontmatter', None)) |
Zack Williams | 1b96a95 | 2017-07-17 16:48:01 -0700 | [diff] [blame] | 180 | |
| 181 | # add descriptions to def_docs |
| 182 | for d_name, d_text in descriptions.iteritems(): |
| 183 | if d_name in def_docs: |
| 184 | def_docs[d_name]['description'] = d_text |
| 185 | else: |
Zack Williams | be0b2c7 | 2018-01-25 14:48:38 -0700 | [diff] [blame] | 186 | LOG.error( |
| 187 | "Description exists for '%s' but doesn't exist in defaults" % |
| 188 | d_name) |
Zack Williams | 1b96a95 | 2017-07-17 16:48:01 -0700 | [diff] [blame] | 189 | |
| 190 | # check for missing descriptions |
| 191 | for key in sorted(def_docs): |
| 192 | if not def_docs[key]['description']: |
| 193 | LOG.error("No description found for '%s'" % key) |
| 194 | |
| 195 | # Add to template and write to output file |
| 196 | j2env = jinja2.Environment( |
Zack Williams | be0b2c7 | 2018-01-25 14:48:38 -0700 | [diff] [blame] | 197 | loader=jinja2.FileSystemLoader('.') |
Zack Williams | 1b96a95 | 2017-07-17 16:48:01 -0700 | [diff] [blame] | 198 | ) |
| 199 | |
| 200 | template = j2env.get_template(args.template) |
| 201 | |
| 202 | with open(args.output, 'w') as f: |
Zack Williams | be0b2c7 | 2018-01-25 14:48:38 -0700 | [diff] [blame] | 203 | f.write(template.render(def_docs=def_docs, frontmatter=frontmatter)) |