summaryrefslogtreecommitdiffstats
path: root/hacking/build_library/build_ansible/command_plugins/dump_keywords.py
blob: e9379317f9671b3e8ecabe6fda2b608023c32451 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# coding: utf-8
# Copyright: (c) 2019, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

import importlib
import os.path
import pathlib
import re
from ansible.module_utils.compat.version import LooseVersion

import jinja2
import yaml
from jinja2 import Environment, FileSystemLoader

from ansible.module_utils._text import to_bytes

# Pylint doesn't understand Python3 namespace modules.
from ..change_detection import update_file_if_different  # pylint: disable=relative-beyond-top-level
from ..commands import Command  # pylint: disable=relative-beyond-top-level


DEFAULT_TEMPLATE_DIR = str(pathlib.Path(__file__).resolve().parents[4] / 'docs/templates')
TEMPLATE_FILE = 'playbooks_keywords.rst.j2'
PLAYBOOK_CLASS_NAMES = ['Play', 'Role', 'Block', 'Task']


def load_definitions(keyword_definitions_file):
    docs = {}
    with open(keyword_definitions_file) as f:
        docs = yaml.safe_load(f)

    return docs


def extract_keywords(keyword_definitions):
    pb_keywords = {}
    for pb_class_name in PLAYBOOK_CLASS_NAMES:
        if pb_class_name == 'Play':
            module_name = 'ansible.playbook'
        else:
            module_name = 'ansible.playbook.{0}'.format(pb_class_name.lower())
        module = importlib.import_module(module_name)
        playbook_class = getattr(module, pb_class_name, None)
        if playbook_class is None:
            raise ImportError("We weren't able to import the module {0}".format(module_name))

        # Maintain order of the actual class names for our output
        # Build up a mapping of playbook classes to the attributes that they hold
        pb_keywords[pb_class_name] = {k: v for (k, v) in playbook_class.fattributes.items()
                                      # Filter private attributes as they're not usable in playbooks
                                      if not v.private}

        # pick up definitions if they exist
        for keyword in tuple(pb_keywords[pb_class_name]):
            if keyword in keyword_definitions:
                pb_keywords[pb_class_name][keyword] = keyword_definitions[keyword]
            else:
                # check if there is an alias, otherwise undocumented
                alias = getattr(playbook_class.fattributes.get(keyword), 'alias', None)
                if alias and alias in keyword_definitions:
                    pb_keywords[pb_class_name][alias] = keyword_definitions[alias]
                    del pb_keywords[pb_class_name][keyword]
                else:
                    pb_keywords[pb_class_name][keyword] = ' UNDOCUMENTED!! '

        # loop is really with_ for users
        if pb_class_name == 'Task':
            pb_keywords[pb_class_name]['with_<lookup_plugin>'] = (
                'The same as ``loop`` but magically adds the output of any lookup plugin to'
                ' generate the item list.')

        # local_action is implicit with action
        if 'action' in pb_keywords[pb_class_name]:
            pb_keywords[pb_class_name]['local_action'] = ('Same as action but also implies'
                                                          ' ``delegate_to: localhost``')

    return pb_keywords


def generate_page(pb_keywords, template_dir):
    env = Environment(loader=FileSystemLoader(template_dir), trim_blocks=True,)
    template = env.get_template(TEMPLATE_FILE)
    tempvars = {'pb_keywords': pb_keywords, 'playbook_class_names': PLAYBOOK_CLASS_NAMES}

    keyword_page = template.render(tempvars)
    if LooseVersion(jinja2.__version__) < LooseVersion('2.10'):
        # jinja2 < 2.10's indent filter indents blank lines.  Cleanup
        keyword_page = re.sub(' +\n', '\n', keyword_page)

    return keyword_page


class DocumentKeywords(Command):
    name = 'document-keywords'

    @classmethod
    def init_parser(cls, add_parser):
        parser = add_parser(cls.name, description='Generate playbook keyword documentation from'
                            ' code and descriptions')
        parser.add_argument("-T", "--template-dir", action="store", dest="template_dir",
                            default=DEFAULT_TEMPLATE_DIR,
                            help="directory containing Jinja2 templates")
        parser.add_argument("-o", "--output-dir", action="store", dest="output_dir",
                            default='/tmp/', help="Output directory for rst files")
        parser.add_argument("keyword_defs", metavar="KEYWORD-DEFINITIONS.yml", type=str,
                            help="Source for playbook keyword docs")

    @staticmethod
    def main(args):
        keyword_definitions = load_definitions(args.keyword_defs)
        pb_keywords = extract_keywords(keyword_definitions)

        keyword_page = generate_page(pb_keywords, args.template_dir)
        outputname = os.path.join(args.output_dir, TEMPLATE_FILE.replace('.j2', ''))
        update_file_if_different(outputname, to_bytes(keyword_page))

        return 0