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