azdev/operations/help/__init__.py (209 lines of code) (raw):
# -----------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for
# license information.
# -----------------------------------------------------------------------------
import os
import sys
import json
import shutil
import tempfile
from subprocess import check_call, check_output, CalledProcessError
from knack.util import CLIError
from knack.log import get_logger
from azure.cli.core.extension.operations import list_available_extensions, list_extensions as list_cli_extensions # pylint: disable=import-error
from azdev.utilities import (
display, heading, subheading,
get_cli_repo_path, get_path_table
)
from azdev.utilities.tools import require_azure_cli
from azdev.operations.extensions import list_extensions as list_dev_cli_extensions
DOC_MAP_NAME = 'doc_source_map.json'
HELP_FILE_NAME = '_help.py'
DOC_SOURCE_MAP_PATH = os.path.join('doc', 'sphinx', 'azhelpgen', DOC_MAP_NAME)
_logger = get_logger(__name__)
def check_document_map():
heading('Verify Document Map')
cli_repo = get_cli_repo_path()
map_path = os.path.join(cli_repo, DOC_SOURCE_MAP_PATH)
help_files_in_map = _get_help_files_in_map(map_path)
help_files_not_found = _map_help_files_not_found(cli_repo, help_files_in_map)
help_files_to_add_to_map = _help_files_not_in_map(cli_repo, help_files_in_map)
subheading('Results')
if help_files_not_found or help_files_to_add_to_map:
error_lines = []
error_lines.append('Errors whilst verifying {}!'.format(DOC_MAP_NAME))
if help_files_not_found:
error_lines.append('The following files are in {} but do not exist:'.format(DOC_MAP_NAME))
error_lines += help_files_not_found
if help_files_to_add_to_map:
error_lines.append('The following files should be added to {}:'.format(DOC_MAP_NAME))
error_lines += help_files_to_add_to_map
error_msg = '\n'.join(error_lines)
raise CLIError(error_msg)
display('Verified {} OK.'.format(DOC_MAP_NAME))
def _get_help_files_in_map(map_path):
with open(map_path) as json_file:
json_data = json.load(json_file)
return [os.path.normpath(x) for x in list(json_data.values())]
def _map_help_files_not_found(cli_repo, help_files_in_map):
missing_files = []
for path in help_files_in_map:
if not os.path.isfile(os.path.normpath(os.path.join(cli_repo, path))):
missing_files.append(path)
return missing_files
def _help_files_not_in_map(cli_repo, help_files_in_map):
not_in_map = []
for _, path in get_path_table()['mod'].items():
help_path = os.path.join(path, HELP_FILE_NAME)
help_path = help_path.replace(cli_repo.lower() + os.sep, '')
if help_path in help_files_in_map or not os.path.isfile(help_path):
continue
not_in_map.append(help_path)
return not_in_map
def generate_cli_ref_docs(output_dir=None, output_type=None, all_profiles=None):
# require that azure cli installed and warn the users if extensions are installed.
require_azure_cli()
output_dir = _process_ref_doc_output_dir(output_dir)
_warn_if_exts_installed()
heading('Generate CLI Reference Docs')
display("Docs will be placed in {}.".format(output_dir))
if all_profiles:
# Generate documentation for all commands and for all CLI profiles
_generate_ref_docs_for_all_profiles(output_type, output_dir)
else:
# Generate documentation for all comamnds
_call_sphinx_build(output_type, output_dir)
display("\nThe {} files are in {}".format(output_type, output_dir))
def generate_extension_ref_docs(output_dir=None, output_type=None):
# require that azure cli installed
require_azure_cli()
output_dir = _process_ref_doc_output_dir(output_dir)
heading('Generate CLI Extensions Reference Docs')
display("Docs will be placed in {}.".format(output_dir))
display("Generating Docs for public extensions. Installed extensions will not be affected...")
_generate_ref_docs_for_public_exts(output_type, output_dir)
display("\nThe {} files are in {}".format(output_type, output_dir))
def _process_ref_doc_output_dir(output_dir):
# handle output_dir
# if non specified, store in "_build" in the current working directory
if not output_dir:
_logger.warning("No output directory was specified. Will use a temporary directory to store reference docs.")
output_dir = tempfile.mkdtemp(prefix="doc_output_")
# ensure output_dir exists otherwise create it
output_dir = os.path.abspath(output_dir)
if not os.path.exists(output_dir):
existing_path = os.path.dirname(output_dir)
base_dir = os.path.basename(output_dir)
if not os.path.exists(existing_path):
raise CLIError("Cannot create output directory {} in non-existent path {}."
.format(base_dir, existing_path))
os.mkdir(output_dir)
return output_dir
def _generate_ref_docs_for_all_profiles(output_type, base_output_dir):
original_profile = None
profile = ""
try:
# store original profile and get all profiles.
original_profile = _get_current_profile()
profiles = _get_profiles()
_logger.info("Original Profile: %s", original_profile)
for profile in profiles:
# set profile and call sphinx build cmd
profile_output_dir = os.path.join(base_output_dir, profile)
_set_profile(profile)
_call_sphinx_build(output_type, profile_output_dir)
display("\nFinished generating files for profile {} in dir {}\n".format(output_type, profile_output_dir))
# always set the profile back to the original profile after generating all docs.
_set_profile(original_profile)
except (CLIError, KeyboardInterrupt, SystemExit) as e:
_logger.error("Error when attempting to generate docs for profile %s.\n\t%s", profile, e)
if original_profile:
_logger.error("Will try to set the CLI's profile back to the original value: '%s'", original_profile)
_set_profile(original_profile)
# still re-raise the error.
raise e
def _generate_ref_docs_for_public_exts(output_type, base_output_dir):
# TODO: this shouldn't define the env key, but should reference it from a central place in the cli repo.
ENV_KEY_AZURE_EXTENSION_DIR = 'AZURE_EXTENSION_DIR'
extensions_url_tups = _get_available_extension_urls()
if not extensions_url_tups:
raise CLIError("Failed to retrieve public extensions.")
temp_dir = tempfile.mkdtemp(prefix="temp_whl_ext_dir")
_logger.debug("Created temp directory to store downloaded whl files: %s", temp_dir)
try:
for name, file_name, download_url in extensions_url_tups:
# for every compatible public extensions
# download the whl file
whl_file_name = _get_whl_from_url(download_url, file_name, temp_dir)
# install the whl file in a new temp directory
installed_ext_dir = tempfile.mkdtemp(prefix="temp_extension_dir_", dir=temp_dir)
_logger.debug("Created temp directory %s to use as the extension installation dir for %s extension.",
installed_ext_dir, name)
pip_cmd = [sys.executable, '-m', 'pip', 'install', '--target',
os.path.join(installed_ext_dir, 'extension'),
whl_file_name, '--disable-pip-version-check', '--no-cache-dir']
display('Executing "{}"'.format(' '.join(pip_cmd)))
check_call(pip_cmd)
# set the directory as the extension directory in the environment used to call sphinx-build
env = os.environ.copy()
env[ENV_KEY_AZURE_EXTENSION_DIR] = installed_ext_dir
# generate documentation for installed extensions
ext_output_dir = os.path.join(base_output_dir, name)
os.makedirs(ext_output_dir)
_call_sphinx_build(output_type, ext_output_dir, for_extensions_alone=True, call_env=env,
msg="\nGenerating ref docs for {}".format(name))
finally:
# finally delete the temp dir
shutil.rmtree(temp_dir)
_logger.debug("Deleted temp whl extension directory: %s", temp_dir)
def _call_sphinx_build(builder_name, output_dir, for_extensions_alone=False, call_env=None, msg=""):
conf_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'refdoc')
if for_extensions_alone:
source_dir = os.path.abspath(os.path.join(conf_dir, 'extension_docs'))
else:
source_dir = os.path.abspath(os.path.join(conf_dir, 'cli_docs'))
try:
opts = ['-E', '-b', builder_name, '-c', conf_dir]
args = [source_dir, output_dir]
if for_extensions_alone:
# apparently the configuration in extensions and core CLI differed in this way. This is only cosmetic
# set smartquotes to false. Due to a bug, one has to use "0" instead "False"
opts.extend(["-D", "smartquotes=0"])
sphinx_cmd = ['sphinx-build'] + opts + args
display("sphinx cmd: {}".format(" ".join(sphinx_cmd)))
display(msg)
# call sphinx-build
check_call(sphinx_cmd, stdout=sys.stdout, stderr=sys.stderr, env=call_env)
except CalledProcessError:
raise CLIError("Doc generation failed.")
def _get_current_profile():
try:
return check_output(['az', 'cloud', 'show', '--query', '"profile"', '-otsv']).decode('utf-8').strip()
except CalledProcessError as e:
raise CLIError("Failed to get current profile due to err: {}".format(e))
def _set_profile(profile):
try:
_logger.warning("Setting the CLI profile to '%s'", profile)
check_call(['az', 'cloud', 'update', '--profile', profile])
except CalledProcessError as e:
raise CLIError("Failed to set profile {} due to err:\n{}\n"
"Please check that your profile is set to the expected value.".format(profile, e))
def _get_profiles():
try:
profiles_str = check_output(["az", "cloud", "list-profiles", "-o", "tsv"]).decode('utf-8').strip()
except CalledProcessError as e:
raise CLIError("Failed to get profiles due to err: {}".format(e))
return profiles_str.splitlines()
def _warn_if_exts_installed():
cli_extensions, dev_cli_extensions = list_cli_extensions(), list_dev_cli_extensions()
if cli_extensions:
_logger.warning("One or more CLI Extensions are installed and will be included in ref doc output.")
if dev_cli_extensions:
_logger.warning(
"One or more CLI Extensions are installed in development mode and will be included in ref doc output.")
if cli_extensions or dev_cli_extensions:
_logger.warning("Please uninstall the extension(s) if you want to generate Core CLI docs solely.")
# Todo, this would be unnecessary if list_available_extensions has a switch for including download urls....
def _get_available_extension_urls():
""" Get download urls for all the CLI extensions compatible with the installed development CLI.
:return: list of 3-tuples in the form of '(extension_name, extension_file_name, extensions_download_url)'
"""
all_pub_extensions = list_available_extensions(show_details=True)
compatible_extensions = list_available_extensions()
name_url_tups = []
for ext in compatible_extensions:
old_length = len(name_url_tups)
ext_name, ext_version = ext["name"], ext["version"]
for ext_info in all_pub_extensions[ext_name]:
if ext_version == ext_info["metadata"]["version"]:
name_url_tups.append((ext_name, ext_info["filename"], ext_info["downloadUrl"]))
break
if old_length == len(name_url_tups):
_logger.warning("'%s' has no versions compatible with the installed CLI's version", ext_name)
return name_url_tups
def _get_whl_from_url(url, filename, tmp_dir, whl_cache=None):
if not whl_cache:
whl_cache = {}
if url in whl_cache:
return whl_cache[url]
import requests
r = requests.get(url, stream=True)
assert r.status_code == 200, "Request to {} failed with {}".format(url, r.status_code)
ext_file = os.path.join(tmp_dir, filename)
with open(ext_file, 'wb') as f:
for chunk in r.iter_content(chunk_size=1024):
if chunk: # ignore keep-alive new chunks
f.write(chunk)
whl_cache[url] = ext_file
return ext_file