-
Notifications
You must be signed in to change notification settings - Fork 129
Expand file tree
/
Copy path__init__.py
More file actions
314 lines (241 loc) · 12.5 KB
/
__init__.py
File metadata and controls
314 lines (241 loc) · 12.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
# -----------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for
# license information.
# -----------------------------------------------------------------------------
from __future__ import print_function
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,
require_virtual_env
)
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