Skip to content

Commit

Permalink
Resource Commands (Azure#304)
Browse files Browse the repository at this point in the history
* Add support to list resources by resource group. Add resource set command
to add or clear tags from a resource.

* Address code review comments and add 'resource provider list' and 'resource provider show' commands.
  • Loading branch information
tjprescott authored and johanste committed May 24, 2016
1 parent 20fe360 commit e1b6120
Show file tree
Hide file tree
Showing 20 changed files with 1,339 additions and 616 deletions.
6 changes: 6 additions & 0 deletions azure-cli.pyproj
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@
<Compile Include="command_modules\azure-cli-resource\azure\cli\command_modules\resource\custom.py" />
<Compile Include="command_modules\azure-cli-resource\azure\cli\command_modules\resource\generated.py" />
<Compile Include="command_modules\azure-cli-resource\azure\cli\command_modules\resource\tests\test_validators.py" />
<Compile Include="command_modules\azure-cli-resource\azure\cli\command_modules\resource\_actions.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="command_modules\azure-cli-resource\azure\cli\command_modules\resource\_factory.py">
<SubType>Code</SubType>
</Compile>
<Compile Include="command_modules\azure-cli-resource\azure\cli\command_modules\resource\_params.py" />
<Compile Include="command_modules\azure-cli-resource\azure\cli\command_modules\resource\_validators.py">
<SubType>Code</SubType>
Expand Down
8 changes: 6 additions & 2 deletions src/azure/cli/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,17 @@
'name': '--tag',
'metavar': 'TAG',
'help': L('a single tag in \'key[=value]\' format'),
'type': validate_tag
'type': validate_tag,
'nargs': '?',
'const': {}
},
'tags' : {
'name': '--tags',
'metavar': 'TAGS',
'help': L('multiple semicolon separated tags in \'key[=value]\' format'),
'type': validate_tags
'type': validate_tags,
'nargs': '?',
'const': {}
},
}

Expand Down
4 changes: 2 additions & 2 deletions src/azure/cli/commands/_validators.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
def validate_tags(string):
''' Extracts multiple tags in key[=value] format, separated by semicolons '''
result = None
result = {}
if string:
result = validate_key_value_pairs(string)
s_list = [x for x in string.split(';') if '=' not in x] # single values
Expand All @@ -9,7 +9,7 @@ def validate_tags(string):

def validate_tag(string):
''' Extracts a single tag in key[=value] format '''
result = None
result = {}
if string:
comps = string.split('=', 1)
result = {comps[0]: comps[1]} if len(comps) > 1 else {string: ''}
Expand Down
2 changes: 1 addition & 1 deletion src/azure/cli/tests/test_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def test_tags_valid(self):
def test_tags_invalid(self):
input = ''
actual = validate_tags(input)
expected = None
expected = {}
self.assertEqual(actual, expected)

def test_tag(self):
Expand Down
2 changes: 2 additions & 0 deletions src/azure/cli/utils/command_test_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ def compare(self, json_data):
def _check_json(source, checks):

def _check_json_child(item, checks):
if not item:
return not checks
for check in checks.keys():
if isinstance(checks[check], dict) and check in item:
return _check_json_child(item[check], checks[check])
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from azure.cli.parser import IncorrectUsageError

from ._factory import _resource_client_factory

def _resolve_api_version(rcf, resource_type, parent=None):

provider = rcf.providers.get(resource_type.namespace)
resource_type_str = '{}/{}'.format(parent.type, resource_type.type) \
if parent else resource_type.type

rt = [t for t in provider.resource_types if t.resource_type == resource_type_str]
if not rt:
raise IncorrectUsageError('Resource type {} not found.'
.format(resource_type_str))
if len(rt) == 1 and rt[0].api_versions:
npv = [v for v in rt[0].api_versions if 'preview' not in v.lower()]
return npv[0] if npv else rt[0].api_versions[0]
else:
raise IncorrectUsageError(
'API version is required and could not be resolved for resource {}/{}'
.format(resource_type.namespace, resource_type.type))

def handle_resource_parameters(**kwargs):
args = vars(kwargs['args'])

param_set = set(['resource_type', 'api_version',
'resource_provider_namespace', 'parent_resource_path'])
if not param_set.issubset(set(args.keys())):
return

resource_tuple = args.get('resource_type')
parent_tuple = args.get('parent_resource_path')

rcf = _resource_client_factory()
args['api_version'] = args.get('api_version') or \
_resolve_api_version(rcf, resource_tuple, parent_tuple)
args['resource_type'] = resource_tuple.type
args['resource_provider_namespace'] = resource_tuple.namespace
args['parent_resource_path'] = '{}/{}'.format(
parent_tuple.type,
parent_tuple.name) if parent_tuple else ''
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from azure.mgmt.resource.resources import (ResourceManagementClient,
ResourceManagementClientConfiguration)

from azure.cli.commands._command_creation import get_mgmt_service_client

def _resource_client_factory(**_):
return get_mgmt_service_client(ResourceManagementClient, ResourceManagementClientConfiguration)
Original file line number Diff line number Diff line change
@@ -1,28 +1,31 @@
from azure.mgmt.resource.resources import (ResourceManagementClient,
ResourceManagementClientConfiguration)
import argparse

from azure.cli.commands import COMMON_PARAMETERS as GLOBAL_COMMON_PARAMETERS, patch_aliases
from azure.cli.commands._command_creation import get_mgmt_service_client
from azure.cli._locale import L

from ._validators import validate_resource_type, validate_parent

# FACTORIES

def _resource_client_factory(**_):
return get_mgmt_service_client(ResourceManagementClient, ResourceManagementClientConfiguration)

# BASIC PARAMETER CONFIGURATION

PARAMETER_ALIASES = patch_aliases(GLOBAL_COMMON_PARAMETERS, {
'api_version': {
'name': '--api-version',
'help': 'The api version of the resource (omit for latest)',
'required': False
},
'resource_provider_namespace': {
'name': '--resource-provider-namespace',
'help': argparse.SUPPRESS,
'required': False
},
'resource_type': {
'name': '--resource-type',
'help': L('the resource type in <namespace>/<type> format'),
'help': 'The resource type in <namespace>/<type> format',
'type': validate_resource_type
},
'parent': {
'parent_resource_path': {
'name': '--parent',
'help': L('the parent resource type in <type>/<name> format'),
'type': validate_parent
'help': 'The parent resource type in <type>/<name> format',
'type': validate_parent,
'required': False
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,38 @@
import json
from codecs import open as codecs_open

from msrestazure.azure_exceptions import CloudError
from azure.mgmt.resource.resources.models.resource_group import ResourceGroup
from azure.mgmt.resource.resources.models import GenericResource

from azure.cli.parser import IncorrectUsageError
from azure.cli.commands import CommandTable
from azure.cli._locale import L
from azure.cli._util import CLIError
import azure.cli._logging as _logging
from azure.cli.commands import LongRunningOperation
from azure.cli.commands._command_creation import get_mgmt_service_client

from ._params import _resource_client_factory
from ._factory import _resource_client_factory

logger = _logging.get_az_logger(__name__)

command_table = CommandTable()

def _list_resources_odata_filter_builder(location=None, resource_type=None, tag=None, name=None):
def _list_resources_odata_filter_builder(location=None, resource_type=None,
resource_group_name=None, tag=None, name=None):
'''Build up OData filter string from parameters
'''

filters = []

if resource_group_name:
filters.append("resourceGroup eq '{}'".format(resource_group_name))

if name:
filters.append("name eq '%s'" % name)
filters.append("name eq '{}'".format(name))

if location:
filters.append("location eq '%s'" % location)
filters.append("location eq '{}'".format(location))

if resource_type:
filters.append("resourceType eq '{}/{}'".format(
Expand All @@ -51,24 +56,6 @@ def _list_resources_odata_filter_builder(location=None, resource_type=None, tag=
filters.append("tagvalue eq '%s'" % tag_value)
return ' and '.join(filters)

def _resolve_api_version(rcf, resource_type, parent=None):

provider = rcf.providers.get(resource_type.namespace)
resource_type_str = '{}/{}'.format(parent.type, resource_type.type) \
if parent else resource_type.type

rt = [t for t in provider.resource_types if t.resource_type == resource_type_str]
if not rt:
raise IncorrectUsageError('Resource type {}/{} not found.'
.format(resource_type.namespace, resource_type.type))
if len(rt) == 1 and rt[0].api_versions:
npv = [v for v in rt[0].api_versions if "preview" not in v]
return npv[0] if npv else rt[0].api_versions[0]
else:
raise IncorrectUsageError(
L('API version is required and could not be resolved for resource {}/{}'
.format(resource_type.namespace, resource_type.type)))

class ConvenienceResourceGroupCommands(object):

def __init__(self, **_):
Expand Down Expand Up @@ -134,36 +121,13 @@ def export_group_as_template(self,

print(json.dumps(result.template, indent=2))


class ConvenienceResourceCommands(object):

def __init__(self, **_):
pass

def show(self, resource_group, resource_name, resource_type, api_version=None, parent=None):
''' Show details of a specific resource in a resource group or subscription
:param str resource_group:the containing resource group name
:param str resource_name:the resource name
:param str resource_type:the resource type in format: <provider-namespace>/<type>
:param str api_version:the API version of the resource provider
:param str parent:the name of the parent resource (if needed) in <type>/<name> format'''
rcf = _resource_client_factory()

api_version = _resolve_api_version(rcf, resource_type, parent) \
if not api_version else api_version
parent_path = '{}/{}'.format(parent.type, parent.name) if parent else ''

results = rcf.resources.get(
resource_group_name=resource_group,
resource_name=resource_name,
resource_provider_namespace=resource_type.namespace,
resource_type=resource_type.type,
api_version=api_version,
parent_resource_path=parent_path
)
return results

def list(self, location=None, resource_type=None, tag=None, name=None):
def list(self, location=None, resource_type=None, resource_group_name=None, tag=None,
name=None):
''' List resources
EXAMPLES:
az resource list --location westus
Expand All @@ -178,7 +142,8 @@ def list(self, location=None, resource_type=None, tag=None, name=None):
:param str name:filter by resource name
'''
rcf = _resource_client_factory()
odata_filter = _list_resources_odata_filter_builder(location, resource_type, tag, name)
odata_filter = _list_resources_odata_filter_builder(
location, resource_type, resource_group_name, tag, name)
resources = rcf.resources.list(filter=odata_filter)
return list(resources)

Expand Down Expand Up @@ -211,6 +176,33 @@ def deploy(self, resource_group, deployment_name, template_file_path,
poller = smc.deployments.create_or_update(resource_group, deployment_name, properties)
return op(poller)

def set_tag(self, resource_group_name, resource_name, resource_type, tags,
parent_resource_path=None, api_version=None, resource_provider_namespace=None):
''' Updates the tags on an existing resource. To clear tags, specify the --tag option
without anything else. '''
rcf = _resource_client_factory()
resource = rcf.resources.get(
resource_group_name,
resource_provider_namespace,
parent_resource_path,
resource_type,
resource_name,
api_version)
parameters = GenericResource(resource.location, tags, None, None) # pylint: disable=no-member
try:
rcf.resources.create_or_update(
resource_group_name,
resource_provider_namespace,
parent_resource_path,
resource_type,
resource_name,
api_version,
parameters)
except CloudError as ex:
# TODO: Remove workaround once Swagger and SDK fix is implemented (#120123723)
if '202' not in str(ex):
raise ex

def _get_file_json(file_path):
return _load_json(file_path, 'utf-8') \
or _load_json(file_path, 'utf-8-sig') \
Expand Down
Loading

0 comments on commit e1b6120

Please sign in to comment.