1# -------------------------------------------------------------------------------------------- 2# Copyright (c) Microsoft Corporation. All rights reserved. 3# Licensed under the MIT License. See License.txt in the project root for license information. 4# -------------------------------------------------------------------------------------------- 5 6import json 7import os 8 9import configparser 10from knack.log import get_logger 11from knack.prompting import prompt, prompt_y_n, prompt_choice_list, prompt_pass, NoTTYException 12from knack.util import CLIError 13 14from azure.cli.core.util import ConfiguredDefaultSetter 15 16from azure.cli.command_modules.configure._consts import (OUTPUT_LIST, LOGIN_METHOD_LIST, 17 MSG_INTRO, 18 MSG_CLOSING, 19 MSG_GLOBAL_SETTINGS_LOCATION, 20 MSG_HEADING_CURRENT_CONFIG_INFO, 21 MSG_HEADING_ENV_VARS, 22 MSG_PROMPT_MANAGE_GLOBAL, 23 MSG_PROMPT_GLOBAL_OUTPUT, 24 MSG_PROMPT_LOGIN, 25 MSG_PROMPT_TELEMETRY, 26 MSG_PROMPT_FILE_LOGGING, 27 MSG_PROMPT_CACHE_TTL, 28 WARNING_CLOUD_FORBID_TELEMETRY, 29 DEFAULT_CACHE_TTL) 30from azure.cli.command_modules.configure._utils import get_default_from_config 31 32answers = {} 33 34logger = get_logger(__name__) 35 36 37def _print_cur_configuration(file_config): 38 from azure.cli.core._config import ENV_VAR_PREFIX 39 print(MSG_HEADING_CURRENT_CONFIG_INFO) 40 for section in file_config.sections(): 41 print() 42 print('[{}]'.format(section)) 43 for option in file_config.options(section): 44 print('{} = {}'.format(option, file_config.get(section, option))) 45 env_vars = [ev for ev in os.environ if ev.startswith(ENV_VAR_PREFIX)] 46 if env_vars: 47 print(MSG_HEADING_ENV_VARS) 48 print('\n'.join(['{} = {}'.format(ev, os.environ[ev]) for ev in env_vars])) 49 50 51def _config_env_public_azure(cli_ctx, _): 52 from adal.adal_error import AdalError 53 from azure.cli.core.commands.client_factory import get_mgmt_service_client 54 from azure.cli.core._profile import Profile 55 from azure.cli.core.profiles import ResourceType 56 # Determine if user logged in 57 58 try: 59 list(get_mgmt_service_client(cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES).resources.list()) 60 except CLIError: 61 # Not logged in 62 login_successful = False 63 while not login_successful: 64 method_index = prompt_choice_list(MSG_PROMPT_LOGIN, LOGIN_METHOD_LIST) 65 answers['login_index'] = method_index 66 answers['login_options'] = str(LOGIN_METHOD_LIST) 67 profile = Profile(cli_ctx=cli_ctx) 68 interactive = False 69 username = None 70 password = None 71 service_principal = None 72 tenant = None 73 if method_index == 0: # device auth 74 interactive = True 75 elif method_index == 1: # username and password 76 username = prompt('Username: ') 77 password = prompt_pass(msg='Password: ') 78 elif method_index == 2: # service principal with secret 79 service_principal = True 80 username = prompt('Service principal: ') 81 tenant = prompt('Tenant: ') 82 password = prompt_pass(msg='Client secret: ') 83 elif method_index == 3: # skip 84 return 85 try: 86 profile.find_subscriptions_on_login( 87 interactive, 88 username, 89 password, 90 service_principal, 91 tenant) 92 login_successful = True 93 logger.warning('Login successful!') 94 except AdalError as err: 95 logger.error('Login error!') 96 logger.error(err) 97 98 99def _handle_global_configuration(config, cloud_forbid_telemetry): 100 # print location of global configuration 101 print(MSG_GLOBAL_SETTINGS_LOCATION.format(config.config_path)) 102 # set up the config parsers 103 file_config = configparser.ConfigParser() 104 config_exists = file_config.read([config.config_path]) 105 should_modify_global_config = False 106 if config_exists: 107 # print current config and prompt to allow global config modification 108 _print_cur_configuration(file_config) 109 should_modify_global_config = prompt_y_n(MSG_PROMPT_MANAGE_GLOBAL, default='n') 110 answers['modify_global_prompt'] = should_modify_global_config 111 if not config_exists or should_modify_global_config: 112 # no config exists yet so configure global config or user wants to modify global config 113 with ConfiguredDefaultSetter(config, False): 114 output_index = prompt_choice_list(MSG_PROMPT_GLOBAL_OUTPUT, OUTPUT_LIST, 115 default=get_default_from_config(config, 116 'core', 'output', 117 OUTPUT_LIST)) 118 answers['output_type_prompt'] = output_index 119 answers['output_type_options'] = str(OUTPUT_LIST) 120 enable_file_logging = prompt_y_n(MSG_PROMPT_FILE_LOGGING, default='n') 121 if cloud_forbid_telemetry: 122 allow_telemetry = False 123 else: 124 allow_telemetry = prompt_y_n(MSG_PROMPT_TELEMETRY, default='y') 125 answers['telemetry_prompt'] = allow_telemetry 126 cache_ttl = None 127 while not cache_ttl: 128 try: 129 cache_ttl = prompt(MSG_PROMPT_CACHE_TTL) or DEFAULT_CACHE_TTL 130 # ensure valid int by casting 131 cache_value = int(cache_ttl) 132 if cache_value < 1: 133 raise ValueError 134 except ValueError: 135 logger.error('TTL must be a positive integer') 136 cache_ttl = None 137 # save the global config 138 config.set_value('core', 'output', OUTPUT_LIST[output_index]['name']) 139 config.set_value('core', 'collect_telemetry', 'yes' if allow_telemetry else 'no') 140 config.set_value('core', 'cache_ttl', cache_ttl) 141 config.set_value('logging', 'enable_log_file', 'yes' if enable_file_logging else 'no') 142 143 144# pylint: disable=inconsistent-return-statements 145def handle_configure(cmd, defaults=None, list_defaults=None, scope=None): 146 from azure.cli.core.cloud import cloud_forbid_telemetry, get_active_cloud_name 147 if defaults: 148 defaults_section = cmd.cli_ctx.config.defaults_section_name 149 with ConfiguredDefaultSetter(cmd.cli_ctx.config, scope.lower() == 'local'): 150 for default in defaults: 151 parts = default.split('=', 1) 152 if len(parts) == 1: 153 raise CLIError('usage error: --defaults STRING=STRING STRING=STRING ...') 154 cmd.cli_ctx.config.set_value(defaults_section, parts[0], _normalize_config_value(parts[1])) 155 return 156 if list_defaults: 157 with ConfiguredDefaultSetter(cmd.cli_ctx.config, scope.lower() == 'local'): 158 defaults_result = cmd.cli_ctx.config.items(cmd.cli_ctx.config.defaults_section_name) 159 return [x for x in defaults_result if x.get('value')] 160 161 # if nothing supplied, we go interactively 162 try: 163 print(MSG_INTRO) 164 cloud_forbid_telemetry = cloud_forbid_telemetry(cmd.cli_ctx) 165 _handle_global_configuration(cmd.cli_ctx.config, cloud_forbid_telemetry) 166 print(MSG_CLOSING) 167 if cloud_forbid_telemetry: 168 logger.warning(WARNING_CLOUD_FORBID_TELEMETRY, get_active_cloud_name(cmd.cli_ctx)) 169 # TODO: log_telemetry('configure', **answers) 170 except NoTTYException: 171 raise CLIError('This command is interactive and no tty available.') 172 except (EOFError, KeyboardInterrupt): 173 print() 174 175 176def _normalize_config_value(value): 177 if value: 178 value = '' if value in ["''", '""'] else value 179 return value 180 181 182def _get_cache_directory(cli_ctx): 183 from azure.cli.core.commands.client_factory import get_subscription_id 184 from azure.cli.core._environment import get_config_dir 185 return os.path.join( 186 get_config_dir(), 187 'object_cache', 188 cli_ctx.cloud.name, 189 get_subscription_id(cli_ctx)) 190 191 192def list_cache_contents(cmd): 193 from glob import glob 194 directory = _get_cache_directory(cmd.cli_ctx) 195 contents = [] 196 rg_paths = glob(os.path.join(directory, '*')) 197 for rg_path in rg_paths: 198 rg_name = os.path.split(rg_path)[1] 199 for dir_name, _, file_list in os.walk(rg_path): 200 if not file_list: 201 continue 202 resource_type = os.path.split(dir_name)[1] 203 for f in file_list: 204 file_path = os.path.join(dir_name, f) 205 try: 206 with open(file_path, 'r') as cache_file: 207 cache_obj = json.loads(cache_file.read()) 208 contents.append({ 209 'resourceGroup': rg_name, 210 'resourceType': resource_type, 211 'name': f.split('.', 1)[0], 212 'lastSaved': cache_obj['last_saved'] 213 }) 214 except KeyError: 215 # invalid cache entry 216 logger.debug('Removing corrupt cache file: %s', file_path) 217 os.remove(file_path) 218 return contents 219 220 221def show_cache_contents(cmd, resource_group_name, item_name, resource_type): 222 directory = _get_cache_directory(cmd.cli_ctx) 223 item_path = os.path.join(directory, resource_group_name, resource_type, '{}.json'.format(item_name)) 224 try: 225 with open(item_path, 'r') as cache_file: 226 cache_obj = json.loads(cache_file.read()) 227 except (OSError, IOError): 228 raise CLIError('Not found in cache: {}'.format(item_path)) 229 return cache_obj['_payload'] 230 231 232def delete_cache_contents(cmd, resource_group_name, item_name, resource_type): 233 directory = _get_cache_directory(cmd.cli_ctx) 234 item_path = os.path.join(directory, resource_group_name, resource_type, '{}.json'.format(item_name)) 235 try: 236 os.remove(item_path) 237 except (OSError, IOError): 238 logger.info('%s not found in object cache.', item_path) 239 240 241def purge_cache_contents(): 242 import shutil 243 from azure.cli.core._environment import get_config_dir 244 directory = os.path.join(get_config_dir(), 'object_cache') 245 try: 246 shutil.rmtree(directory) 247 except (OSError, IOError) as ex: 248 logger.debug(ex) 249 250 251def turn_local_context_on(cmd): 252 if not cmd.cli_ctx.local_context.is_on: 253 cmd.cli_ctx.local_context.turn_on() 254 logger.warning('Local context is turned on, you can run `az local-context off` to turn it off.') 255 else: 256 logger.warning('Local context is on already.') 257 258 259def turn_local_context_off(cmd): 260 if cmd.cli_ctx.local_context.is_on: 261 cmd.cli_ctx.local_context.turn_off() 262 logger.warning('Local context is turned off, you can run `az local-context on` to turn it on.') 263 else: 264 logger.warning('Local context is off already.') 265 266 267def show_local_context(cmd, name=None): 268 return cmd.cli_ctx.local_context.get_value(name) 269 270 271def delete_local_context(cmd, name=None, all=False, yes=False, purge=False, recursive=False): # pylint: disable=redefined-builtin 272 if name: 273 return cmd.cli_ctx.local_context.delete(name) 274 275 if all: 276 from azure.cli.core.util import user_confirmation 277 if purge: 278 user_confirmation('You are going to delete local context persistence file. ' 279 'Are you sure you want to continue this operation ?', yes) 280 cmd.cli_ctx.local_context.delete_file(recursive) 281 else: 282 user_confirmation('You are going to clear all local context value. ' 283 'Are you sure you want to continue this operation ?', yes) 284 cmd.cli_ctx.local_context.clear(recursive) 285