# Copyright 2017 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Sphinx extension for pretty-formatting policy docs.""" import os from docutils import nodes from docutils.parsers import rst from docutils.parsers.rst import directives from docutils import statemachine from oslo_config import cfg from sphinx.util import logging from sphinx.util.nodes import nested_parse_with_titles from oslo_policy import generator def _indent(text): """Indent by four spaces.""" prefix = ' ' * 4 def prefixed_lines(): for line in text.splitlines(True): yield (prefix + line if line.strip() else line) return ''.join(prefixed_lines()) def _format_policy_rule(rule): """Output a definition list-style rule. For example:: ``os_compute_api:servers:create`` :Default: ``rule:admin_or_owner`` :Operations: - **POST** ``/servers`` Create a server """ yield '``{}``'.format(rule.name) if rule.check_str: yield _indent(':Default: ``{}``'.format(rule.check_str)) else: yield _indent(':Default: ') if hasattr(rule, 'operations'): yield _indent(':Operations:') for operation in rule.operations: yield _indent(_indent('- **{}** ``{}``'.format( operation['method'], operation['path']))) if hasattr(rule, 'scope_types') and rule.scope_types is not None: yield _indent(':Scope Types:') for scope_type in rule.scope_types: yield _indent(_indent('- **{}**'.format(scope_type))) yield '' if rule.description: for line in rule.description.strip().splitlines(): yield _indent(line.rstrip()) else: yield _indent('(no description provided)') yield '' def _format_policy_section(section, rules): # The nested_parse_with_titles will ensure the correct header leve is used. yield section yield '=' * len(section) yield '' for rule in rules: for line in _format_policy_rule(rule): yield line def _format_policy(namespaces): policies = generator.get_policies_dict(namespaces) for section in sorted(policies.keys()): for line in _format_policy_section(section, policies[section]): yield line class ShowPolicyDirective(rst.Directive): has_content = False option_spec = { 'config-file': directives.unchanged, } def run(self): env = self.state.document.settings.env app = env.app config_file = self.options.get('config-file') # if the config_file option was not defined, attempt to reuse the # 'oslo_policy.sphinxpolicygen' extension's setting if not config_file and hasattr(env.config, 'policy_generator_config_file'): config_file = env.config.policy_generator_config_file # If we are given a file that isn't an absolute path, look for it # in the source directory if it doesn't exist. candidates = [ config_file, os.path.join(app.srcdir, config_file,), ] for c in candidates: if os.path.isfile(c): config_path = c break else: raise ValueError( 'could not find config file in: %s' % str(candidates) ) self.info('loading config file %s' % config_path) conf = cfg.ConfigOpts() opts = generator.GENERATOR_OPTS + generator.RULE_OPTS conf.register_cli_opts(opts) conf.register_opts(opts) conf( args=['--config-file', config_path], ) namespaces = conf.namespace[:] result = statemachine.ViewList() source_name = '<' + __name__ + '>' for line in _format_policy(namespaces): result.append(line, source_name) node = nodes.section() node.document = self.state.document # With the resolution for bug #1788183, we now parse the # 'DocumentedRuleDefault.description' attribute as rST. Unfortunately, # there are a lot of broken option descriptions out there and we don't # want to break peoples' builds suddenly. As a result, we disable # 'warning-is-error' temporarily. Users will still see the warnings but # the build will continue. with logging.skip_warningiserror(): nested_parse_with_titles(self.state, result, node) return node.children def setup(app): app.add_directive('show-policy', ShowPolicyDirective) return { 'parallel_read_safe': True, 'parallel_write_safe': True, }