openstack-doc-tools/os_doc_tools/commands.py

674 lines
25 KiB
Python

#!/usr/bin/env python
# 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.
import argparse
import os
import subprocess
import sys
import os_doc_tools
from os_doc_tools.common import check_output # noqa
def use_help_flag(os_command):
"""Use --help flag (instead of help keyword)
Returns true if the command requires a --help flag instead
of a help keyword.
"""
return os_command == "swift" or "-manage" in os_command
def quote_xml(line):
"""Convert special characters for XML output."""
line = line.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
if 'DEPRECATED!' in line:
line = line.replace('DEPRECATED!', '<emphasis>DEPRECATED!</emphasis>')
elif 'DEPRECATED' in line:
line = line.replace('DEPRECATED', '<emphasis>DEPRECATED</emphasis>')
if 'env[' in line:
line = line.replace('env[', '<code>env[').replace(']', ']</code>')
return line
def generate_heading(os_command, api_name, title, os_file):
"""Write DocBook file header.
:param os_command: client command to document
:param api_name: string description of the API of os_command
:param os_file: open filehandle for output of DocBook file
"""
try:
version = check_output([os_command, "--version"],
stderr=subprocess.STDOUT)
except OSError as e:
if e.errno == os.errno.ENOENT:
print("Command %s not found, aborting." % os_command)
sys.exit(1)
# Extract version from "swift 0.3"
version = version.strip().rpartition(' ')[2]
print("Documenting '%s help (version %s)'" % (os_command, version))
if use_help_flag(os_command):
help_str = "<replaceable>COMMAND</replaceable> <option>--help</option>"
else:
help_str = "<option>help</option> <replaceable>COMMAND</replaceable>"
header1 = """<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<chapter xmlns=\"http://docbook.org/ns/docbook\"
xmlns:xi=\"http://www.w3.org/2001/XInclude\"
xmlns:xlink=\"http://www.w3.org/1999/xlink\"
version=\"5.0\"
xml:id=\"%(os_command)sclient_commands\">
<!-- This file is automatically generated, do not edit -->
<?dbhtml stop-chunking?>
<title>%(title)s</title>\n"""
if os_command == "openstack":
header2 = """
<para>The <command>%(os_command)s</command> client is a common
OpenStack command-line interface (CLI).\n"""
else:
header2 = """
<para>The <command>%(os_command)s</command> client is the command-line
interface (CLI) for the %(api_name)s and its extensions.\n"""
header3 = """
This chapter documents <command>%(os_command)s</command> version
%(version)s.
</para>
<para>For help on a specific <command>%(os_command)s</command>
command, enter:
</para>
<screen><prompt>$</prompt> <userinput><command>%(os_command)s</command> \
%(help_str)s</userinput></screen>
<section xml:id=\"%(os_command)sclient_command_usage\">
<title>%(os_command)s usage</title>\n"""
format_dict = {
"os_command": os_command,
"api_name": api_name,
"title": title,
"version": version,
"help_str": help_str
}
os_file.write(header1 % format_dict)
os_file.write(header2 % format_dict)
os_file.write(header3 % format_dict)
def is_option(string):
"""Returns True if string specifies an argument."""
for x in string:
if not (x.isupper() or x == '_' or x == ','):
return False
if string.startswith('DEPRECATED'):
return False
return True
def extract_options(line):
"""Extract command or option from line."""
# We have a command or parameter to handle
# Differentiate:
# 1. --version
# 2. --timeout <seconds>
# 3. --service <service>, --service-id <service>
# 4. -v, --verbose
# 5. -p PORT, --port PORT
# 6. <backup> ID of the backup to restore.
# 7. --alarm-action <Webhook URL>
# 8. <NAME or ID> Name or ID of stack to resume.
split_line = line.split(None, 2)
if split_line[0].startswith("-"):
last_was_option = True
else:
last_was_option = False
if (len(split_line) > 1 and
('<' in split_line[0] or
'<' in split_line[1] or
'--' in split_line[1] or
split_line[1].startswith(("-", '<', '{', '[')) or
is_option(split_line[1]))):
words = line.split(None)
i = 0
while i < len(words) - 1:
if (('<' in words[i] and
'>' not in words[i]) or
('[' in words[i] and
']' not in words[i])):
words[i] += ' ' + words[i + 1]
del words[i + 1]
else:
i += 1
while len(words) > 1:
if words[1].startswith('DEPRECATED'):
break
if last_was_option:
if (words[1].startswith(("-", '<', '{', '[')) or
is_option(words[1])):
words[0] = words[0] + ' ' + words[1]
del words[1]
else:
break
else:
if words[1].startswith("-"):
words[0] = words[0] + ' ' + words[1]
del words[1]
else:
break
w0 = words[0]
del words[0]
w1 = ''
if len(words) > 0:
w1 = words[0]
del words[0]
for w in words:
w1 += " " + w
if len(w1) == 0:
split_line = [w0]
else:
split_line = [w0, w1]
else:
split_line = line.split(None, 1)
return split_line
def format_table(title, lines, os_file):
"""Nicely print section of lines."""
close_entry = False
os_file.write(" <variablelist wordsize=\"10\">\n")
if len(title) > 0:
os_file.write(" <title>%s</title>\n" % title)
for line in lines:
if len(line) == 0 or line[0] != ' ':
break
# We have to handle these cases:
# 1. command Explanation
# 2. command
# Explanation on next line
# 3. command Explanation continued
# on next line
# If there are more than 8 spaces, let's treat it as
# explanation.
if line.startswith(' '):
# Explanation
os_file.write(" %s\n" % quote_xml(line.lstrip(' ')))
continue
# Now we have a command or parameter to handle
split_line = extract_options(line)
if not close_entry:
close_entry = True
else:
os_file.write(" </para>\n")
os_file.write(" </listitem>\n")
os_file.write(" </varlistentry>\n")
os_file.write(" <varlistentry>\n")
os_file.write(" <term><command>%s</command></term>\n"
% quote_xml(split_line[0]))
os_file.write(" <listitem>\n")
os_file.write(" <para>\n")
if len(split_line) > 1:
os_file.write(" %s\n" % quote_xml(split_line[1]))
os_file.write(" </para>\n")
os_file.write(" </listitem>\n")
os_file.write(" </varlistentry>\n")
os_file.write(" </variablelist>\n")
return
def generate_command(os_command, os_file):
"""Convert os_command --help to DocBook.
:param os_command: client command to document
:param os_file: open filehandle for output of DocBook file
"""
help_lines = check_output([os_command, "--help"]).split('\n')
ignore_next_lines = False
next_line_screen = True
next_line_screen = True
line_index = -1
in_screen = False
for line in help_lines:
line_index += 1
xline = quote_xml(line)
if len(line) > 0 and line[0] != ' ':
# XXX: Might have whitespace before!!
if '<subcommands>' in line:
ignore_next_lines = False
continue
if 'Positional arguments' in line:
ignore_next_lines = True
next_line_screen = True
os_file.write("</computeroutput></screen>\n")
in_screen = False
format_table('Subcommands', help_lines[line_index + 2:],
os_file)
continue
if line.startswith(('Optional arguments:', 'Optional:',
'Options:', 'optional arguments')):
if in_screen:
os_file.write("</computeroutput></screen>\n")
in_screen = False
os_file.write(" </section>\n")
os_file.write(" <section ")
os_file.write("xml:id=\"%sclient_command_optional\">\n"
% os_command)
os_file.write(" <title>%s optional arguments</title>\n"
% os_command)
format_table('', help_lines[line_index + 1:],
os_file)
next_line_screen = True
ignore_next_lines = True
continue
# neutron
if line.startswith('Commands for API v2.0:'):
if in_screen:
os_file.write("</computeroutput></screen>\n")
in_screen = False
os_file.write(" </section>\n")
os_file.write(" <section ")
os_file.write("xml:id=\"%sclient_command_api_2_0\">\n"
% os_command)
os_file.write(" <title>%s API v2.0 commands</title>\n"
% os_command)
format_table('', help_lines[line_index + 1:],
os_file)
next_line_screen = True
ignore_next_lines = True
continue
# swift
if line.startswith('Examples:'):
os_file.write(" </section>\n")
os_file.write(" <section ")
os_file.write("xml:id=\"%sclient_command_examples\">\n"
% os_command)
os_file.write(" <title>%s examples</title>\n"
% os_command)
next_line_screen = True
ignore_next_lines = False
continue
if not line.startswith('usage'):
continue
if not ignore_next_lines:
if next_line_screen:
os_file.write(" <screen><computeroutput>%s" % xline)
next_line_screen = False
in_screen = True
elif len(line) > 0:
os_file.write("\n%s" % xline.rstrip())
if in_screen:
os_file.write("</computeroutput></screen>\n")
os_file.write(" </section>\n")
def generate_subcommand(os_command, os_subcommand, os_file):
"""Convert os_command help os_subcommand to DocBook.
:param os_command: client command to document
:param os_subcommand: client subcommand to document
:param os_file: open filehandle for output of DocBook file
"""
if use_help_flag(os_command):
help_lines = check_output([os_command, os_subcommand,
"--help"]).split('\n')
else:
help_lines = check_output([os_command, "help",
os_subcommand]).split('\n')
os_subcommandid = os_subcommand.replace(' ', '_')
os_file.write(" <section xml:id=\"%sclient_subcommand_%s\">\n"
% (os_command, os_subcommandid))
os_file.write(" <title>%s %s command</title>\n"
% (os_command, os_subcommand))
if os_command == "swift":
next_line_screen = False
os_file.write("\n <screen><computeroutput>Usage: swift %s"
"</computeroutput></screen>"
% (os_subcommand))
os_file.write("\n <para>")
in_para = True
else:
next_line_screen = True
in_para = False
line_index = -1
# Content is:
# usage...
#
# Description
#
# Arguments
skip_lines = False
for line in help_lines:
line_index += 1
if line.startswith('Usage:') and os_command == "swift":
line = line[len("Usage: "):]
if line.startswith(('Arguments:', 'Positional arguments:',
'positional arguments', 'Optional arguments',
'optional arguments')):
if in_para:
in_para = False
os_file.write("\n </para>")
if line.startswith(('Positional arguments',
'positional arguments')):
format_table('Positional arguments',
help_lines[line_index + 1:], os_file)
skip_lines = True
continue
elif line.startswith(('Optional arguments:',
'optional arguments')):
format_table('Optional arguments',
help_lines[line_index + 1:], os_file)
break
else:
format_table('Arguments', help_lines[line_index + 1:], os_file)
break
if skip_lines:
continue
if len(line) == 0:
if not in_para:
os_file.write("</computeroutput></screen>")
os_file.write("\n <para>")
in_para = True
continue
xline = quote_xml(line)
if next_line_screen:
os_file.write(" <screen><computeroutput>%s" % xline)
next_line_screen = False
else:
os_file.write("\n%s" % (xline))
if in_para:
os_file.write("\n </para>\n")
os_file.write(" </section>\n")
def generate_subcommands(os_command, os_file, blacklist, only_subcommands):
"""Convert os_command help subcommands for all subcommands to DocBook.
:param os_command: client command to document
:param os_file: open filehandle for output of DocBook file
:param blacklist: list of elements that will not be documented
:param only_subcommands: if not empty, list of subcommands to document
"""
print("Documenting '%s' subcommands..." % os_command)
blacklist.append("bash-completion")
blacklist.append("complete")
blacklist.append("help")
if not only_subcommands:
all_options = check_output([os_command,
"bash-completion"]).strip().split()
else:
all_options = only_subcommands
subcommands = [o for o in all_options if not
(o.startswith('-') or o in blacklist)]
for subcommand in sorted(subcommands):
generate_subcommand(os_command, subcommand, os_file)
print ("%d subcommands documented." % len(subcommands))
def generate_end(os_file):
"""Finish writing file.
:param os_file: open filehandle for output of DocBook file
"""
print("Finished.\n")
os_file.write("</chapter>\n")
def document_single_project(os_command, output_dir):
"""Create documenation for os_command."""
print ("Documenting '%s'" % os_command)
blacklist = []
subcommands = []
if os_command == 'ceilometer':
api_name = "Telemetry API"
title = "Telemetry command-line client"
blacklist = ["alarm-create"]
elif os_command == 'cinder':
api_name = "OpenStack Block Storage API"
title = "Block Storage command-line client"
elif os_command == 'glance':
api_name = 'OpenStack Image Service API'
title = "Image Service command-line client"
# Does not know about bash-completion yet, need to specify
# subcommands manually
subcommands = ["image-create", "image-delete", "image-list",
"image-show", "image-update", "member-create",
"member-delete", "member-list"]
elif os_command == 'heat':
api_name = "Orchestration API"
title = "Orchestration command-line client"
blacklist = ["create", "delete", "describe", "event",
"gettemplate", "list", "resource",
"update", "validate"]
elif os_command == 'ironic':
api_name = "Bare metal"
title = "Bare metal command-line client"
# Does not know about bash-completion yet, need to specify
# subcommands manually
subcommands = ["chassis-create", "chassis-delete", "chassis-list",
"chassis-node-list", "chassis-show", "chassis-update",
"driver-list", "node-create", "node-delete",
"node-list", "node-port-list", "node-set-power-state",
"node-show", "node-update", "node-validate",
"port-create", "port-delete", "port-list", "port-show",
"port-update"]
elif os_command == 'keystone':
api_name = "OpenStack Identity API"
title = "Identity service command-line client"
elif os_command == 'neutron':
api_name = "OpenStack Networking API"
title = "Networking command-line client"
elif os_command == 'nova':
api_name = "OpenStack Compute API"
title = "Compute command-line client"
blacklist = ["add-floating-ip", "remove-floating-ip"]
elif os_command == 'sahara':
api_name = "Data processing API"
title = "Data processing command-line client"
elif os_command == 'swift':
api_name = "OpenStack Object Storage API"
title = "Object Storage command-line client"
# Does not know about bash-completion yet, need to specify
# subcommands manually
subcommands = ["delete", "download", "list", "post",
"stat", "upload"]
elif os_command == 'trove':
api_name = "Database API"
title = "Database Service command-line client"
blacklist = ["resize-flavor"]
elif os_command == 'trove-manage':
api_name = "Database Management Utility"
title = "Database Service Management command-line client"
# Does not know about bash-completion yet, need to specify
# subcommands manually
subcommands = ["db_sync", "db_upgrade",
"db_downgrade", "datastore_update",
"datastore_version_update", "db_recreate"]
elif os_command == 'openstack':
api_name = ''
title = "OpenStack client"
# Does not support bash-completion command in the form
# required by this tool.
subcommands = ["aggregate add host", "aggregate create",
"aggregate delete", "aggregate list",
"aggregate remove host", "aggregate set",
"aggregate show", "backup create",
"backup delete", "backup list",
"backup restore", "backup show",
"compute agent create", "compute agent delete",
"compute agent list", "compute agent set",
"compute service list", "compute service set",
"console log show", "console url show",
"container list", "container show",
"ec2 credentials create", "ec2 credentials delete",
"ec2 credentials list", "ec2 credentials show",
"endpoint create", "endpoint delete",
"endpoint list", "endpoint show",
"extension list",
"flavor create", "flavor delete",
"flavor list", "flavor show",
"host list",
"host show", "hypervisor list",
"hypervisor show", "image create",
"image delete", "image list",
"image save", "image set",
"image show", "ip fixed add",
"ip fixed remove", "ip floating add",
"ip floating create", "ip floating delete",
"ip floating list", "ip floating pool list",
"ip floating remove", "keypair create",
"keypair delete", "keypair list",
"keypair show", "limits show",
"module list", "object list",
"object show", "project create",
"project delete", "project list",
"project set", "project show",
"project usage list", "quota set",
"quota show", "role add",
"role create", "role delete",
"role list", "role remove",
"role show", "security group create",
"security group delete", "security group list",
"security group rule create",
"security group rule delete",
"security group rule list", "security group set",
"security group show", "server add security group",
"server add volume", "server create",
"server delete", "server image create",
"server list", "server lock",
"server migrate", "server pause",
"server reboot", "server rebuild",
"server remove security group", "server remove volume",
"server rescue", "server resize",
"server resume", "server set",
"server show", "server ssh",
"server suspend", "server unlock",
"server unpause", "server unrescue",
"server unset", "service create",
"service delete", "service list",
"service show", "snapshot create",
"snapshot delete", "snapshot list",
"snapshot set", "snapshot show",
"token issue", "token revoke",
"user create",
"user delete", "user list",
"user role list", "user set",
"user show", "volume create",
"volume delete", "volume list",
"volume set", "volume show",
"volume type create", "volume type delete",
"volume type list", "volume type set",
"volume type unset", "volume unset"]
else:
print("'%s' command not yet handled" % os_command)
sys.exit(-1)
out_filename = "ch_cli_" + os_command + "_commands.xml"
out_file = open(os.path.join(output_dir, out_filename), 'w')
generate_heading(os_command, api_name, title, out_file)
generate_command(os_command, out_file)
generate_subcommands(os_command, out_file, blacklist,
subcommands)
generate_end(out_file)
out_file.close()
def main():
print("OpenStack Auto Documenting of Commands (using "
"openstack-doc-tools version %s)\n"
% os_doc_tools.__version__)
api_clients = ["ceilometer", "cinder", "glance", "heat", "keystone",
"nova", "neutron", "swift", "trove"]
manage_clients = ["trove-manage"]
all_clients = api_clients + manage_clients
parser = argparse.ArgumentParser(description="Generate DocBook XML files "
"to document python-PROJECTclients.")
parser.add_argument('client', nargs='?',
help="OpenStack command to document. One of: " +
", ".join(all_clients) + ".")
parser.add_argument("--all", help="Document all clients. "
"Namely " + ", ".join(all_clients) + ".",
action="store_true")
parser.add_argument("--all-api", help="Document all API clients. "
"Namely " + ", ".join(api_clients) + ".",
action="store_true")
parser.add_argument("--all-manage", help="Document all manage clients. "
"Namely " + ", ".join(manage_clients) + ".",
action="store_true")
parser.add_argument("--output-dir", default=".",
help="Directory to write generated files to")
prog_args = parser.parse_args()
if prog_args.all or prog_args.all_api or prog_args.all_manage:
if prog_args.all or prog_args.all_api:
for client in api_clients:
document_single_project(client, prog_args.output_dir)
if prog_args.all or prog_args.all_manage:
for client in manage_clients:
document_single_project(client, prog_args.output_dir)
elif prog_args.client is None:
parser.print_help()
sys.exit(1)
else:
document_single_project(prog_args.client, prog_args.output_dir)
if __name__ == "__main__":
sys.exit(main())