#!/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 DEVNULL = open(os.devnull, 'wb') 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('&', '&').replace('<', '<').replace('>', '>') if 'DEPRECATED!' in line: line = line.replace('DEPRECATED!', 'DEPRECATED!') elif 'DEPRECATED' in line: line = line.replace('DEPRECATED', 'DEPRECATED') if 'env[' in line: line = line.replace('env[', 'env[').replace(']', ']') 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.splitlines()[-1].strip().rpartition(' ')[2] print("Documenting '%s help (version %s)'" % (os_command, version)) if use_help_flag(os_command): help_str = "COMMAND " else: help_str = " COMMAND" header1 = """ %(title)s\n""" if os_command == "openstack": header2 = """ The %(os_command)s client is a common OpenStack command-line interface (CLI).\n""" else: header2 = """ The %(os_command)s client is the command-line interface (CLI) for the %(api_name)s and its extensions.\n""" header3 = """ This chapter documents %(os_command)s version %(version)s. For help on a specific %(os_command)s command, enter: $ %(os_command)s \ %(help_str)s
%(os_command)s usage\n""" if os_command == "keystone": header_deprecation = """ The %(os_command)s CLI is deprecated in favor of python-openstackclient. For a Python library, continue using python-%(os_command)sclient. \n""" else: header_deprecation = None format_dict = { "os_command": os_command, "api_name": api_name, "title": title, "version": version, "help_str": help_str } os_file.write(header1 % format_dict) if header_deprecation: os_file.write(header_deprecation % 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 # 3. --service , --service-id # 4. -v, --verbose # 5. -p PORT, --port PORT # 6. ID of the backup to restore. # 7. --alarm-action # 8. 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(" \n") if len(title) > 0: os_file.write(" %s\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(" \n") os_file.write(" \n") os_file.write(" \n") os_file.write(" \n") os_file.write(" %s\n" % quote_xml(split_line[0])) os_file.write(" \n") os_file.write(" \n") if len(split_line) > 1: os_file.write(" %s\n" % quote_xml(split_line[1])) os_file.write(" \n") os_file.write(" \n") os_file.write(" \n") os_file.write(" \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"], stderr=DEVNULL).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 '' in line: ignore_next_lines = False continue if 'Positional arguments' in line: ignore_next_lines = True next_line_screen = True os_file.write("\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("\n") in_screen = False os_file.write("
\n") os_file.write("
\n" % os_command) os_file.write(" %s optional arguments\n" % os_command) format_table('', help_lines[line_index + 1:], os_file) next_line_screen = True ignore_next_lines = True continue # sahara if line.startswith('Common auth options'): if in_screen: os_file.write("\n") in_screen = False os_file.write("
\n") os_file.write("
\n" % os_command) os_file.write(" %s common authentication " "arguments\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("\n") in_screen = False os_file.write("
\n") os_file.write("
\n" % os_command) os_file.write(" %s API v2.0 commands\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("
\n") os_file.write("
\n" % os_command) os_file.write(" %s examples\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(" %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("\n") os_file.write("
\n") def generate_subcommand(os_command, os_subcommand, os_file, extra_params, suffix, title_suffix): """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 :param extra_params: Extra parameter to pass to os_command :param suffix: Extra suffix to add to xml:id :param title_suffix: Extra suffix for title """ args = [os_command] if extra_params: args.extend(extra_params) if use_help_flag(os_command): args.append(os_subcommand) args.append("--help") else: args.append("help") args.append(os_subcommand) help_lines = check_output(args, stderr=DEVNULL).split('\n') os_subcommandid = os_subcommand.replace(' ', '_') os_file.write("
\n" % (os_command, os_subcommandid, suffix)) os_file.write(" %s %s%s\n" % (os_command, os_subcommand, title_suffix)) if os_command == "swift": next_line_screen = False os_file.write("\n Usage: swift %s" "" % (os_subcommand)) os_file.write("\n ") in_para = True else: next_line_screen = True in_para = False if extra_params: extra_paramstr = ' '.join(extra_params) help_lines[0] = help_lines[0].replace(os_command, "%s %s" % (os_command, extra_paramstr)) 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 ") 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("") os_file.write("\n ") in_para = True continue xline = quote_xml(line) if next_line_screen: os_file.write(" %s" % xline) next_line_screen = False else: os_file.write("\n%s" % (xline)) if in_para: os_file.write("\n \n") os_file.write("
\n") def generate_subcommands(os_command, os_file, blacklist, only_subcommands, extra_params, suffix, title_suffix): """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 :param extra_params: Extra parameter to pass to os_command. :param suffix: Extra suffix to add to xml:id :param title_suffix: Extra suffix for title """ print("Documenting '%s' subcommands..." % os_command) blacklist.append("bash-completion") blacklist.append("complete") blacklist.append("help") if not only_subcommands: args = [os_command] if extra_params: args.extend(extra_params) args.append("bash-completion") all_options = check_output(args).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, extra_params, suffix, title_suffix) 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("
\n") def get_openstack_subcommands(commands): """Get all subcommands of 'openstack' without using bashcompletion.""" subcommands = [] for command in commands: output = check_output(["openstack", "--os-auth-type", "token", "help", command], stderr=DEVNULL) for line in output.split("\n"): if line.strip().startswith(command): subcommands.append(line.strip()) return subcommands 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" 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" 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", "capabilities", "tempurl"] 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 know about bash-completion yet, need to specify # commands manually and to fetch subcommands automatically commands = ["aggregate", "availability", "backup", "catalog", "command", "compute", "console", "container", "ec2", "endpoint", "extension", "flavor", "host", "hypervisor", "image", "ip", "keypair", "limits", "module", "network", "object", "project", "quota", "role", "security", "server", "service", "snapshot", "token", "usage", "user", "volume"] subcommands = get_openstack_subcommands(commands) 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) if os_command == 'cinder': out_file.write("""
Block Storage API v1 commands\n""") if os_command == 'glance': out_file.write("""
Image service API v1 commands\n""") if os_command == 'openstack': generate_subcommands(os_command, out_file, blacklist, subcommands, ["--os-auth-type", "token"], "", "") else: generate_subcommands(os_command, out_file, blacklist, subcommands, None, "", "") if os_command == 'cinder': out_file.write("
\n") out_file.write("""
Block Storage API v2 commands You can select an API version to use by adding the --os-volume-api-version parameter or by setting the corresponding environment variable:\n""") out_file.write("$ " "export OS_VOLUME_API_VERSION=2\n" "\n") generate_subcommands(os_command, out_file, blacklist, subcommands, ["--os-volume-api-version", "2"], "_v2", " (v2)") out_file.write("
\n") if os_command == 'glance': out_file.write("""
\n
Image service API v2 commands You can select an API version to use by adding the --os-image-api-version parameter or by setting the corresponding environment variable:\n""") out_file.write("$ " "export OS_IMAGE_API_VERSION=2\n" "\n") generate_subcommands(os_command, out_file, blacklist, subcommands, ["--os-image-api-version", "2"], "_v2", " (v2)") out_file.write("
\n") 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", "ironic", "keystone", "nova", "neutron", "openstack", "sahara", "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())