Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/sphinx/azhelpgen/doc_source_map.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"az": "src/azure-cli/azure/cli/command_modules/profile/_help.py",
"config": "src/azure-cli/azure/cli/command_modules/config/_help.py",
"configure": "src/azure-cli/azure/cli/command_modules/configure/_help.py",
"feedback": "src/azure-cli/azure/cli/command_modules/feedback/_help.py",
"login": "src/azure-cli/azure/cli/command_modules/profile/_help.py",
Expand Down
15 changes: 15 additions & 0 deletions linter_exclusions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,21 @@ cloud update:
cloud_name:
rule_exclusions:
- no_parameter_defaults_for_update_commands
config set:
parameters:
key_value:
rule_exclusions:
- no_positional_parameters
config get:
parameters:
key:
rule_exclusions:
- no_positional_parameters
config unset:
parameters:
key:
rule_exclusions:
- no_positional_parameters
cosmosdb collection create:
parameters:
db_resource_group_name:
Expand Down
5 changes: 4 additions & 1 deletion src/azure-cli-core/azure/cli/core/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -895,7 +895,7 @@ def _log_response(response, **kwargs):
return response


class ConfiguredDefaultSetter:
class ScopedConfig:

def __init__(self, cli_config, use_local_config=None):
self.use_local_config = use_local_config
Expand All @@ -912,6 +912,9 @@ def __exit__(self, exc_type, exc_val, exc_tb):
setattr(self.cli_config, 'use_local_config', self.original_use_local_config)


ConfiguredDefaultSetter = ScopedConfig


def _ssl_context():
if sys.version_info < (3, 4) or (in_cloud_console() and platform.system() == 'Windows'):
try:
Expand Down
2 changes: 1 addition & 1 deletion src/azure-cli-core/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
'colorama~=0.4.1',
'humanfriendly>=4.7,<9.0',
'jmespath',
'knack==0.7.1',
'knack==0.7.2',
'msal~=1.0.0',
'msal-extensions~=0.1.3',
'msrest>=0.4.4',
Expand Down
26 changes: 26 additions & 0 deletions src/azure-cli/azure/cli/command_modules/config/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from azure.cli.core import AzCommandsLoader

import azure.cli.command_modules.config._help # pylint: disable=unused-import


class ConfigCommandsLoader(AzCommandsLoader):

def __init__(self, cli_ctx=None):
super(ConfigCommandsLoader, self).__init__(cli_ctx=cli_ctx)

def load_command_table(self, args):
from azure.cli.command_modules.config.commands import load_command_table
load_command_table(self, args)
return self.command_table

def load_arguments(self, command):
from azure.cli.command_modules.config._params import load_arguments
load_arguments(self, command)


COMMAND_LOADER_CLS = ConfigCommandsLoader
55 changes: 55 additions & 0 deletions src/azure-cli/azure/cli/command_modules/config/_help.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from knack.help_files import helps # pylint: disable=unused-import
# pylint: disable=line-too-long, too-many-lines

helps['config'] = """
type: group
short-summary: Manage Azure CLI configuration.
"""

helps['config set'] = """
type: command
short-summary: Set a configuration.
long-summary: |
For available configuration options, see https://docs.microsoft.com/en-us/cli/azure/azure-cli-configuration.
By default without specifying --local, the configuration will be saved to `~/.azure/config`.
examples:
- name: Disable color with `core.no_color`.
text: az config set core.no_color=true
- name: Hide warnings and only show errors with `core.only_show_errors`.
text: az config set core.only_show_errors=true
- name: Turn on client-side telemetry.
text: az config set core.collect_telemetry=true
- name: Turn on file logging and set its location.
text: |-
az config set logging.enable_log_file=true
az config set logging.log_dir=~/az-logs
- name: Set the default location to `westus2` and default resource group to `myRG`.
text: az config set defaults.location=westus2 defaults.group=MyResourceGroup
- name: Set the default resource group to `myRG` on a local scope.
text: az config set defaults.group=myRG --local
"""

helps['config get'] = """
type: command
short-summary: Get a configuration.
examples:
- name: Get all configurations.
text: az config get
- name: Get configurations in `core` section.
text: az config get core
- name: Get the configuration of key `core.no_color`.
text: az config get core.no_color
"""

helps['config unset'] = """
type: command
short-summary: Unset a configuration.
examples:
- name: Unset the configuration of key `core.no_color`.
text: az config unset core.no_color
"""
30 changes: 30 additions & 0 deletions src/azure-cli/azure/cli/command_modules/config/_params.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

# pylint: disable=line-too-long


def load_arguments(self, _):
with self.argument_context('config') as c:
c.ignore('_subscription') # ignore the global subscription param

with self.argument_context('config set') as c:
c.positional('key_value', nargs='+', help="Space-separated configurations in the form of <section>.<key>=<value>.")
c.argument('local', action='store_true', help='Set as a local configuration in the working directory.')

with self.argument_context('config get') as c:
c.positional('key', nargs='?', help='The configuration to get. '
'If not provided, all sections and configurations will be listed. '
'If `section` is provided, all configurations under the specified section will be listed. '
'If `<section>.<key>` is provided, only the corresponding configuration is shown.')
c.argument('local', action='store_true',
help='Include local configuration. Scan from the working directory up to the root drive, then the global configuration '
'and return the first occurrence.')

with self.argument_context('config unset') as c:
c.positional('key', nargs='+', help='The configuration to unset, in the form of <section>.<key>.')
c.argument('local', action='store_true',
help='Include local configuration. Scan from the working directory up to the root drive, then the global configuration '
'and unset the first occurrence.')
16 changes: 16 additions & 0 deletions src/azure-cli/azure/cli/command_modules/config/commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from azure.cli.core.commands import CliCommandType


def load_command_table(self, _):

config_custom = CliCommandType(operations_tmpl='azure.cli.command_modules.config.custom#{}')

with self.command_group('config', config_custom, is_experimental=True) as g:
g.command('set', 'config_set')
g.command('get', 'config_get')
g.command('unset', 'config_unset')
88 changes: 88 additions & 0 deletions src/azure-cli/azure/cli/command_modules/config/custom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from knack.log import get_logger
from knack.util import CLIError

from azure.cli.core.util import ScopedConfig

logger = get_logger(__name__)


def _normalize_config_value(value):
if value:
value = '' if value in ["''", '""'] else value
return value


def config_set(cmd, key_value=None, local=False):
if key_value:
with ScopedConfig(cmd.cli_ctx.config, local):
for kv in key_value:
# core.no_color=true
parts = kv.split('=', 1)
if len(parts) == 1:
raise CLIError('usage error: [section].[name]=[value] ...')
key = parts[0]
value = parts[1]

# core.no_color
parts = key.split('.', 1)
if len(parts) == 1:
raise CLIError('usage error: [section].[name]=[value] ...')
section = parts[0]
name = parts[1]

cmd.cli_ctx.config.set_value(section, name, _normalize_config_value(value))


def config_get(cmd, key=None, local=False):
# No arg. List all sections and all items
if not key:
with ScopedConfig(cmd.cli_ctx.config, local):
sections = cmd.cli_ctx.config.sections()
result = {}
for section in sections:
items = cmd.cli_ctx.config.items(section)
result[section] = items
return result

parts = key.split('.', 1)
if len(parts) == 1:
# Only section is provided
section = key
name = None
else:
# section.name
section = parts[0]
name = parts[1]

with ScopedConfig(cmd.cli_ctx.config, local):
items = cmd.cli_ctx.config.items(section)

if not name:
# Only section
return items

# section.option
try:
return next(x for x in items if x['name'] == name)
except StopIteration:
raise CLIError("Configuration '{}' is not set.".format(key))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another option for this case is to return empty string. Personally, I prefer empty string for this case.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty string means the config is actually set to '', which is different from the concept of "not set".

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sense to me. Maybe we should also confirm this with PM.



def config_unset(cmd, key=None, local=False):
for k in key:
# section.name
parts = k.split('.', 1)

if len(parts) == 1:
raise CLIError("usage error: [section].[name]")

section = parts[0]
name = parts[1]

with ScopedConfig(cmd.cli_ctx.config, local):
cmd.cli_ctx.config.remove_option(section, name)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What will be returned if key is not set?

Copy link
Member Author

@jiasli jiasli Jul 27, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will ignore the operation and return False. See remove_option:

remove_option(section, option)
Remove the specified option from the specified section. If the section does not exist, raise NoSectionError. If the option existed to be removed, return True; otherwise return False.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, configparser.NoSectionError is ignored by Knack.

https://github.com/microsoft/knack/blob/148173e6fd02bd721920e6b33c9f508b4d584ae9/knack/config.py#L219-L223

            try:
                existed = self.config_parser.remove_option(section, option)
                self.set(self.config_parser)
            except configparser.NoSectionError:
                pass

We'd better confirm with PM about the behavior later.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with your suggestion.

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# --------------------------------------------------------------------------------------------
# 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 tempfile
import unittest
from unittest.mock import MagicMock

from azure.cli.testsdk import ScenarioTest, LocalContextScenarioTest
from knack.util import CLIError


class ConfigTest(ScenarioTest):

def test_config(self):

# [test_section1]
# test_option1 = test_value1
#
# [test_section2]
# test_option21 = test_value21
# test_option22 = test_value22

# C:\Users\{username}\AppData\Local\Temp
tempdir = tempfile.gettempdir()
original_path = os.getcwd()
os.chdir(tempdir)
print("Using temp dir: {}".format(tempdir))

global_test_args = {"source": os.path.expanduser(os.path.join('~', '.azure', 'config')), "flag": ""}
local_test_args = {"source": os.path.join(tempdir, '.azure', 'config'), "flag": " --local"}

for args in (global_test_args, local_test_args):
test_option1_expected = {'name': 'test_option1', 'source': args["source"], 'value': 'test_value1'}
test_option21_expected = {'name': 'test_option21', 'source': args["source"], 'value': 'test_value21'}
test_option22_expected = {'name': 'test_option22', 'source': args["source"], 'value': 'test_value22'}

test_section1_expected = [test_option1_expected]
test_section2_expected = [test_option21_expected, test_option22_expected]

# 1. set
# Test setting one option
self.cmd('config set test_section1.test_option1=test_value1' + args['flag'])
# Test setting multiple options
self.cmd('config set test_section2.test_option21=test_value21 test_section2.test_option22=test_value22' + args['flag'])

# 2. get
# 2.1 Test get all sections
output = self.cmd('config get' + args['flag']).get_output_in_json()
self.assertListEqual(output['test_section1'], test_section1_expected)
self.assertListEqual(output['test_section2'], test_section2_expected)

# 2.2 Test get one section
output = self.cmd('config get test_section1' + args['flag']).get_output_in_json()
self.assertListEqual(output, test_section1_expected)
output = self.cmd('config get test_section2' + args['flag']).get_output_in_json()
self.assertListEqual(output, test_section2_expected)

# 2.3 Test get one item
output = self.cmd('config get test_section1.test_option1' + args['flag']).get_output_in_json()
self.assertDictEqual(output, test_option1_expected)
output = self.cmd('config get test_section2.test_option21' + args['flag']).get_output_in_json()
self.assertDictEqual(output, test_option21_expected)
output = self.cmd('config get test_section2.test_option22' + args['flag']).get_output_in_json()
self.assertDictEqual(output, test_option22_expected)

with self.assertRaises(CLIError):
self.cmd('config get test_section1.test_option22' + args['flag'])

# 3. unset
# Test unsetting one option
self.cmd('config unset test_section1.test_option1' + args['flag'])
# Test unsetting multiple options
self.cmd('config unset test_section2.test_option21 test_section2.test_option22' + args['flag'])

os.chdir(original_path)


if __name__ == '__main__':
unittest.main()
2 changes: 1 addition & 1 deletion src/azure-cli/requirements.py3.Darwin.txt
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ isodate==0.6.0
Jinja2==2.10.1
jmespath==0.9.5
jsmin==2.2.2
knack==0.7.1
knack==0.7.2
MarkupSafe==1.1.1
mock==4.0.2
msrestazure==0.6.3
Expand Down
2 changes: 1 addition & 1 deletion src/azure-cli/requirements.py3.Linux.txt
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ isodate==0.6.0
Jinja2==2.10.1
jmespath==0.9.5
jsmin==2.2.2
knack==0.7.1
knack==0.7.2
MarkupSafe==1.1.1
mock==4.0.2
msrest==0.6.9
Expand Down
2 changes: 1 addition & 1 deletion src/azure-cli/requirements.py3.windows.txt
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ isodate==0.6.0
Jinja2==2.10.1
jmespath==0.9.5
jsmin==2.2.2
knack==0.7.1
knack==0.7.2
MarkupSafe==1.1.1
mock==4.0.2
msrest==0.6.9
Expand Down