WIP! Rework autohelp to use oslo.config
Make use of the smarts built into oslo.config by hacking the autohelp. You can run this like so: cd autogenerate_config_docs ./autohelp2-wrapper -i ../.. -o /tmp generate nova Assuming a directory called 'openstack' with the nova and openstack-doc-tools projects inside. Change-Id: Ib06043a17b1b741fbd944142e06d9f46c44fbf00 Signed-off-by: Stephen Finucane <sfinucan@redhat.com>
This commit is contained in:
parent
e136fd3a56
commit
b361f5c400
|
@ -0,0 +1,166 @@
|
|||
#!/bin/bash
|
||||
# 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.
|
||||
|
||||
set -e
|
||||
|
||||
HERE=$(pwd)
|
||||
SOURCESDIR=$HERE/sources
|
||||
MANUALSREPO=$SOURCESDIR/openstack-manuals
|
||||
AUTOHELP="$HERE/autohelp2.py"
|
||||
GITBASE=${GITBASE:-git://git.openstack.org/openstack}
|
||||
GITPROJ=${GITPROJ:-git://git.openstack.org/openstack}
|
||||
PROJECTS="aodh ceilometer cinder glance heat ironic keystone manila \
|
||||
murano neutron nova sahara senlin trove zaqar"
|
||||
MANUALS_PROJECTS="openstack-manuals"
|
||||
BRANCH=master
|
||||
CLONE_MANUALS=1
|
||||
FAST=0
|
||||
QUIET=0
|
||||
|
||||
usage() {
|
||||
echo "Wrapper for autohelp.py"
|
||||
echo "Usage:"
|
||||
echo " $(basename "$0") [ OPTIONS ] generate|dump|update [ project... ]"
|
||||
echo
|
||||
echo "Subcommands:"
|
||||
echo " generate Generate the options tables in RST format"
|
||||
echo " dump Dumps the list of options with their attributes"
|
||||
echo " update Update or create the flagmapping files"
|
||||
echo
|
||||
echo "Options:"
|
||||
echo " -b BRANCH Work on this branch (defaults to master)"
|
||||
echo " -g GITPROJ Use this location for the project git repos "
|
||||
echo " (defaults to git://git.openstack.org/openstack)"
|
||||
echo " -f Work offline: Do not change environment or sources"
|
||||
echo " -v LEVEL Verbose message (1 or 2)"
|
||||
echo " (check various python modules imported or not)"
|
||||
echo " -i INDIR Path to input sources directory; if specified, fast "
|
||||
echo " mode is implicitly enabled to prevent overwriting "
|
||||
echo " data (defaults to ./sources/)"
|
||||
echo " -o OUTDIR Path to output openstack-manuals directory "
|
||||
echo " (defaults to ./sources/openstack-manuals)"
|
||||
}
|
||||
|
||||
get_project() {
|
||||
project=$1
|
||||
git_url=$GITPROJ
|
||||
|
||||
if [ ! -e "$SOURCESDIR/$project" ]; then
|
||||
if [[ $MANUALS_PROJECTS =~ (^| )$project($| ) ]]; then
|
||||
git_url=$GITBASE
|
||||
fi
|
||||
|
||||
git clone "$git_url/$project" "$SOURCESDIR/$project"
|
||||
else
|
||||
if [ "$project" != openstack-manuals ]; then
|
||||
(cd "$SOURCESDIR/$project" && git pull)
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
while getopts b:g:i:o:fq opt; do
|
||||
case $opt in
|
||||
b)
|
||||
BRANCH=$OPTARG
|
||||
;;
|
||||
g)
|
||||
GITPROJ=$OPTARG
|
||||
;;
|
||||
i)
|
||||
SOURCESDIR=$OPTARG
|
||||
FAST=1
|
||||
;;
|
||||
o)
|
||||
MANUALSREPO=$OPTARG
|
||||
CLONE_MANUALS=0
|
||||
;;
|
||||
f)
|
||||
FAST=1
|
||||
;;
|
||||
q)
|
||||
QUIET=1
|
||||
;;
|
||||
\?)
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
shift $(($OPTIND - 1))
|
||||
|
||||
if [ $# -lt 1 ]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ACTION=$1
|
||||
shift
|
||||
|
||||
if [ $QUIET -eq 1 ]; then
|
||||
exec 3>&1 >/dev/null
|
||||
exec 4>&2 2>/dev/null
|
||||
fi
|
||||
|
||||
case $ACTION in
|
||||
generate|update|dump) ;;
|
||||
*)
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ ! -e autohelp.py ]; then
|
||||
echo "Execute this script in the autogenerate_config_docs directory."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
[ $# != 0 ] && PROJECTS="$*"
|
||||
|
||||
if [ "$CLONE_MANUALS" -eq 1 ] && [ "$FAST" -eq 0 ] ; then
|
||||
get_project openstack-manuals
|
||||
fi
|
||||
|
||||
if [ "$FAST" -eq 0 ] ; then
|
||||
for project in $PROJECTS; do
|
||||
get_project "$project"
|
||||
|
||||
(
|
||||
pushd "$SOURCESDIR/$project"
|
||||
|
||||
GIT_CMD="git show-ref --verify --quiet refs/heads/$BRANCH"
|
||||
if $GIT_CMD; then
|
||||
git checkout "$BRANCH"
|
||||
else
|
||||
git checkout -b "$BRANCH" "remotes/origin/$BRANCH"
|
||||
fi
|
||||
|
||||
popd
|
||||
)
|
||||
done
|
||||
fi
|
||||
|
||||
for project in $PROJECTS; do
|
||||
echo "Working on $project..."
|
||||
|
||||
case $ACTION in
|
||||
generate)
|
||||
pushd "$SOURCESDIR/$project"
|
||||
tox -e venv "python $AUTOHELP generate $project -o $MANUALSREPO"
|
||||
popd
|
||||
;;
|
||||
update)
|
||||
;;
|
||||
dump)
|
||||
;;
|
||||
esac
|
||||
done
|
|
@ -0,0 +1,278 @@
|
|||
# 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.
|
||||
|
||||
"""Generate oslo.config help.
|
||||
|
||||
Hacked version of the 'oslo.config.sphinxext' module that outputs rST files
|
||||
rather than generating the rST dynamically at runtime. This means we don't need
|
||||
to keep the source/environment around at docs build time, but introduces a
|
||||
manual step to updating docs.
|
||||
|
||||
This will do the job until we move the config guides back to the project teams.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_config import generator
|
||||
import oslo_i18n
|
||||
import six
|
||||
|
||||
|
||||
_TYPE_DESCRIPTIONS = {
|
||||
cfg.StrOpt: 'string',
|
||||
cfg.BoolOpt: 'boolean',
|
||||
cfg.IntOpt: 'integer',
|
||||
cfg.FloatOpt: 'floating point',
|
||||
cfg.ListOpt: 'list',
|
||||
cfg.DictOpt: 'dict',
|
||||
cfg.MultiStrOpt: 'multi-valued',
|
||||
cfg.IPOpt: 'ip address',
|
||||
cfg.PortOpt: 'port number',
|
||||
cfg.HostnameOpt: 'hostname',
|
||||
cfg.URIOpt: 'URI',
|
||||
cfg.HostAddressOpt: 'host address',
|
||||
cfg._ConfigFileOpt: 'list of filenames',
|
||||
cfg._ConfigDirOpt: 'list of directory names',
|
||||
}
|
||||
|
||||
|
||||
def _list_table(headers, data, title='', columns=None):
|
||||
"""Build a list-table directive.
|
||||
|
||||
:param add: Function to add one row to output.
|
||||
:param headers: List of header values.
|
||||
:param data: Iterable of row data, yielding lists or tuples with rows.
|
||||
"""
|
||||
yield '.. list-table:: %s' % title
|
||||
yield ' :header-rows: 1'
|
||||
if columns:
|
||||
yield ' :widths: %s' % (','.join(str(c) for c in columns))
|
||||
yield ''
|
||||
yield ' - * %s' % headers[0]
|
||||
for h in headers[1:]:
|
||||
yield ' * %s' % h
|
||||
for row in data:
|
||||
yield ' - * %s' % row[0]
|
||||
for r in row[1:]:
|
||||
yield ' * %s' % r
|
||||
yield ''
|
||||
|
||||
|
||||
def _indent(text):
|
||||
"""Indent by two spaces."""
|
||||
prefix = ' ' * 2
|
||||
|
||||
def prefixed_lines():
|
||||
for line in text.splitlines(True):
|
||||
yield (prefix + line if line.strip() else line)
|
||||
|
||||
return ''.join(prefixed_lines())
|
||||
|
||||
|
||||
def _get_choice_text(choice):
|
||||
if choice is None:
|
||||
return '<None>'
|
||||
elif choice == '':
|
||||
return "''"
|
||||
return six.text_type(choice)
|
||||
|
||||
|
||||
def _format_group(namespace, group_name, group_obj, opt_list):
|
||||
group_name = group_name or 'DEFAULT'
|
||||
|
||||
yield '.. oslo.config:group:: %s' % group_name
|
||||
if namespace:
|
||||
yield ' :namespace: %s' % namespace
|
||||
yield ''
|
||||
|
||||
if group_obj and group_obj.help:
|
||||
yield _indent(group_obj.help.rstrip())
|
||||
yield ''
|
||||
|
||||
for opt in opt_list:
|
||||
opt_type = _TYPE_DESCRIPTIONS.get(type(opt),
|
||||
'unknown type')
|
||||
yield '.. oslo.config:option:: %s' % opt.dest
|
||||
yield ''
|
||||
yield _indent(':Type: %s' % opt_type)
|
||||
for default in generator._format_defaults(opt):
|
||||
if default:
|
||||
default = '``' + default + '``'
|
||||
yield _indent(':Default: %s' % default)
|
||||
if getattr(opt.type, 'min', None) is not None:
|
||||
yield _indent(':Minimum Value: %s' % opt.type.min)
|
||||
if getattr(opt.type, 'max', None) is not None:
|
||||
yield _indent(':Maximum Value: %s' % opt.type.max)
|
||||
if getattr(opt.type, 'choices', None):
|
||||
choices_text = ', '.join([_get_choice_text(choice)
|
||||
for choice in opt.type.choices])
|
||||
yield _indent(':Valid Values: %s' % choices_text)
|
||||
try:
|
||||
if opt.mutable:
|
||||
yield _indent(
|
||||
':Mutable: This option can be changed without restarting.',
|
||||
)
|
||||
except AttributeError as err:
|
||||
# NOTE(dhellmann): keystoneauth defines its own Opt class,
|
||||
# and neutron (at least) returns instances of those
|
||||
# classes instead of oslo_config Opt instances. The new
|
||||
# mutable attribute is the first property where the API
|
||||
# isn't supported in the external class, so we can use
|
||||
# this failure to emit a warning. See
|
||||
# https://bugs.launchpad.net/keystoneauth/+bug/1548433 for
|
||||
# more details.
|
||||
import warnings
|
||||
if not isinstance(cfg.Opt, opt):
|
||||
warnings.warn(
|
||||
'Incompatible option class for %s (%r): %s' %
|
||||
(opt.dest, opt.__class__, err),
|
||||
)
|
||||
else:
|
||||
warnings.warn('Failed to fully format sample for %s: %s' %
|
||||
(opt.dest, err))
|
||||
if opt.advanced:
|
||||
yield _indent(
|
||||
':Advanced Option: intended for advanced users and not used',)
|
||||
yield _indent(
|
||||
':by the majority of users, and might have a significant',)
|
||||
yield _indent(
|
||||
':effect on stability and/or performance.',)
|
||||
yield ''
|
||||
|
||||
try:
|
||||
help_text = opt.help % {'default': 'the value above'}
|
||||
except (TypeError, KeyError, ValueError):
|
||||
# There is no mention of the default in the help string,
|
||||
# the string had some unknown key, or the string contained
|
||||
# invalid formatting characters
|
||||
help_text = opt.help
|
||||
if help_text:
|
||||
yield _indent(help_text)
|
||||
yield ''
|
||||
|
||||
if opt.deprecated_opts:
|
||||
for line in _list_table(
|
||||
['Group', 'Name'],
|
||||
((d.group or group_name,
|
||||
d.name or opt.dest or 'UNSET')
|
||||
for d in opt.deprecated_opts),
|
||||
title='Deprecated Variations'):
|
||||
yield _indent(line)
|
||||
if opt.deprecated_for_removal:
|
||||
yield _indent('.. warning::')
|
||||
if opt.deprecated_since:
|
||||
yield _indent(' This option is deprecated for removal '
|
||||
'since %s.' % opt.deprecated_since)
|
||||
else:
|
||||
yield _indent(' This option is deprecated for removal.')
|
||||
yield _indent(' Its value may be silently ignored in the '
|
||||
'future.')
|
||||
yield ''
|
||||
# Assume that deprecated_reason is never more than plain text...
|
||||
if opt.deprecated_reason:
|
||||
deprecated_text = ' '.join([
|
||||
x.strip() for x in opt.deprecated_reason.splitlines()])
|
||||
yield _indent(' :Reason: ' + deprecated_text)
|
||||
yield ''
|
||||
|
||||
yield ''
|
||||
|
||||
|
||||
def _format_option_help(namespaces, requested_group):
|
||||
"""Generate a series of lines of restructuredtext.
|
||||
|
||||
Format the option help as restructuredtext and return it as a list
|
||||
of lines.
|
||||
"""
|
||||
opts = generator._list_opts(namespaces)
|
||||
|
||||
# Merge the options from different namespaces that belong to
|
||||
# the same group together and format them without the
|
||||
# namespace.
|
||||
by_section = {}
|
||||
group_objs = {}
|
||||
for ignore, opt_list in opts:
|
||||
for group_obj, group_opts in opt_list:
|
||||
if isinstance(group_obj, cfg.OptGroup):
|
||||
group_name = group_obj.name
|
||||
else:
|
||||
group_name = group_obj
|
||||
group_obj = None
|
||||
|
||||
# if we specified a certain group, ignore everything else
|
||||
if requested_group and group_name != requested_group:
|
||||
continue
|
||||
|
||||
group_objs.setdefault(group_name, group_obj)
|
||||
by_section.setdefault(group_name, []).extend(group_opts)
|
||||
|
||||
# this will only a single iteration long if we specified a group above
|
||||
for group_name, group_opts in sorted(by_section.items()):
|
||||
lines = _format_group(
|
||||
namespace=None,
|
||||
group_name=group_name,
|
||||
group_obj=group_objs.get(group_name),
|
||||
opt_list=group_opts,
|
||||
)
|
||||
for line in lines:
|
||||
yield line
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Auto-generate config reference documentation',
|
||||
usage='%(prog)s <cmd> <package> [options]')
|
||||
parser.add_argument('subcommand',
|
||||
choices=['generate', 'update', 'dump'],
|
||||
help='Action (generate, update, dump).')
|
||||
parser.add_argument('project',
|
||||
help='name of the top-level project')
|
||||
parser.add_argument('-n', '--namespace',
|
||||
dest='namespaces',
|
||||
nargs='+',
|
||||
type=str,
|
||||
help='oslo.config namespace(s) to extract options '
|
||||
'from')
|
||||
parser.add_argument('-g', '--group',
|
||||
dest='groups',
|
||||
nargs='*',
|
||||
type=str,
|
||||
help='optional group(s) to generate documentation '
|
||||
'for')
|
||||
parser.add_argument('-o', '--output-dir',
|
||||
dest='output_dir',
|
||||
required=False,
|
||||
type=str,
|
||||
help='directory in which data will be saved; '
|
||||
'defaults to ../../doc/common/tables/ '
|
||||
'for "generate" and stdout for "dump"')
|
||||
args = parser.parse_args()
|
||||
|
||||
# config_file = None # we should support this later (see
|
||||
# # 'nova/etc/nova.conf.ini')
|
||||
|
||||
if args.subcommand == 'generate':
|
||||
# this needs to be restructured so it outputs all groups in separate
|
||||
# files by default
|
||||
for group in args.groups:
|
||||
output_path = '%s/table-%s-%s.rst' % (args.output_dir,
|
||||
args.project, group)
|
||||
with open(output_path, 'w') as output_file:
|
||||
for line in _format_option_help(args.namespaces, group):
|
||||
print(line.rstrip(), file=output_file)
|
||||
else:
|
||||
pass # TODO(stephenfin): Implement the other options
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
Loading…
Reference in New Issue