From 4e0772aedadbfe1810dfa1fd18e17a20039fc469 Mon Sep 17 00:00:00 2001 From: Derek Bekoe Date: Mon, 1 Aug 2016 10:14:43 -0700 Subject: [PATCH 1/3] Table format support + table format tests --- src/azure/cli/_output.py | 57 +++++++-- src/azure/cli/application.py | 11 +- src/azure/cli/commands/__init__.py | 14 ++- src/azure/cli/extensions/query.py | 1 + src/azure/cli/main.py | 2 +- src/azure/cli/tests/test_output.py | 112 +++++++++++++++--- .../cli/command_modules/network/generated.py | 2 +- .../command_modules/storage/_command_type.py | 4 +- .../cli/command_modules/storage/generated.py | 3 +- .../azure/cli/command_modules/vm/generated.py | 4 +- .../vm/tests/test_vm_parameters.py | 6 +- 11 files changed, 168 insertions(+), 48 deletions(-) diff --git a/src/azure/cli/_output.py b/src/azure/cli/_output.py index 347f3c73d7c..4b6791bec09 100644 --- a/src/azure/cli/_output.py +++ b/src/azure/cli/_output.py @@ -8,9 +8,15 @@ import sys import json import re +import traceback from collections import OrderedDict from six import StringIO, text_type, u +from azure.cli._util import CLIError +import azure.cli._logging as _logging + +logger = _logging.get_az_logger(__name__) + def _decode_str(output): if not isinstance(output, text_type): output = u(str(output)) @@ -23,27 +29,37 @@ def default(self, obj): #pylint: disable=method-hidden return json.JSONEncoder.default(self, obj) def format_json(obj): - input_dict = obj.__dict__ if hasattr(obj, '__dict__') else obj + result = obj.result + input_dict = result.__dict__ if hasattr(result, '__dict__') else result return json.dumps(input_dict, indent=2, sort_keys=True, cls=ComplexEncoder, separators=(',', ': ')) + '\n' def format_table(obj): - obj_list = obj if isinstance(obj, list) else [obj] - to = TableOutput() + result = obj.result try: + if obj.simple_output_query and not obj.is_query_active: + from jmespath import compile as compile_jmespath, search, Options + result = compile_jmespath(obj.simple_output_query).search(result, Options(OrderedDict)) + obj_list = result if isinstance(result, list) else [result] + to = TableOutput() for item in obj_list: - for item_key in sorted(item): + for item_key in item: to.cell(item_key, item[item_key]) to.end_row() return to.dump() - except TypeError: - return '' + except (ValueError, KeyError, TypeError): + logger.debug(traceback.format_exc()) + raise CLIError("Table output unavailable. "\ + "Change output type with --output or use "\ + "the --query option to specify an appropriate query. "\ + "Use --debug for more info.") def format_text(obj): - obj_list = obj if isinstance(obj, list) else [obj] + result = obj.result + result_list = result if isinstance(result, list) else [result] to = TextOutput() try: - for item in obj_list: + for item in result_list: for item_key in sorted(item): to.add(item_key, item[item_key]) return to.dump() @@ -51,13 +67,22 @@ def format_text(obj): return '' def format_list(obj): - obj_list = obj if isinstance(obj, list) else [obj] + result = obj.result + result_list = result if isinstance(result, list) else [result] lo = ListOutput() - return lo.dump(obj_list) + return lo.dump(result_list) def format_tsv(obj): - obj_list = obj if isinstance(obj, list) else [obj] - return TsvOutput.dump(obj_list) + result = obj.result + result_list = result if isinstance(result, list) else [result] + return TsvOutput.dump(result_list) + +class CommandResultItem(object): #pylint: disable=too-few-public-methods + + def __init__(self, result, simple_output_query=None, is_query_active=False): + self.result = result + self.simple_output_query = simple_output_query + self.is_query_active = is_query_active class OutputProducer(object): #pylint: disable=too-few-public-methods @@ -81,7 +106,6 @@ def out(self, obj): print(output.encode('ascii', 'ignore').decode('utf-8', 'ignore'), file=self.file, end='') - @staticmethod def get_formatter(format_type): return OutputProducer.format_dict.get(format_type, format_list) @@ -158,6 +182,9 @@ def dump(self, data): return result class TableOutput(object): + + unsupported_types = (list, dict, set) + def __init__(self): self._rows = [{}] self._columns = {} @@ -185,6 +212,10 @@ def any_rows(self): return len(self._rows) > 1 def cell(self, name, value): + if isinstance(value, TableOutput.unsupported_types): + raise TypeError('Table output does not support objects of type {}.\n'\ + 'Offending object name={} value={}'.format( + [ut.__name__ for ut in TableOutput.unsupported_types], name, value)) n = str(name) v = str(value) max_width = self._columns.get(n) diff --git a/src/azure/cli/application.py b/src/azure/cli/application.py index ed20703752f..afb6488640b 100644 --- a/src/azure/cli/application.py +++ b/src/azure/cli/application.py @@ -12,6 +12,7 @@ import argparse from enum import Enum from .parser import AzCliCommandParser +from azure.cli._output import CommandResultItem import azure.cli.extensions import azure.cli._help as _help import azure.cli._logging as _logging @@ -56,7 +57,8 @@ def __init__(self, config=None): 'x-ms-client-request-id': str(uuid.uuid1()) }, 'command': 'unknown', - 'completer_active': ARGCOMPLETE_ENV_NAME in os.environ + 'completer_active': ARGCOMPLETE_ENV_NAME in os.environ, + 'query_active': False } @@ -124,7 +126,10 @@ def execute(self, argv): event_data = {'result': results} self.raise_event(self.TRANSFORM_RESULT, event_data=event_data) self.raise_event(self.FILTER_RESULT, event_data=event_data) - return event_data['result'] + return CommandResultItem(event_data['result'], + simple_output_query= + command_table[args.command].simple_output_query, + is_query_active=self.session['query_active']) def raise_event(self, name, **kwargs): '''Raise the event `name`. @@ -188,7 +193,7 @@ def _register_builtin_arguments(**kwargs): global_group = kwargs['global_group'] global_group.add_argument('--subscription', dest='_subscription_id', help=argparse.SUPPRESS) global_group.add_argument('--output', '-o', dest='_output_format', - choices=['json', 'tsv', 'list'], + choices=['json', 'tsv', 'list', 'table'], default='json', help='Output format', type=str.lower) diff --git a/src/azure/cli/commands/__init__.py b/src/azure/cli/commands/__init__.py index c05ac3feab7..1d247621d07 100644 --- a/src/azure/cli/commands/__init__.py +++ b/src/azure/cli/commands/__init__.py @@ -16,6 +16,7 @@ from msrestazure.azure_operation import AzureOperationPoller from azure.cli._util import CLIError import azure.cli._logging as _logging + from ._introspection import extract_args_from_signature logger = _logging.get_az_logger(__name__) @@ -134,12 +135,13 @@ def wrapped(func): class CliCommand(object): - def __init__(self, name, handler, description=None): + def __init__(self, name, handler, description=None, simple_output_query=None): self.name = name self.handler = handler self.description = description self.help = None self.arguments = {} + self.simple_output_query = simple_output_query def add_argument(self, param_name, *option_strings, **kwargs): argument = CliCommandArgument( @@ -201,11 +203,13 @@ def register_extra_cli_argument(command, dest, **kwargs): ''' _cli_extra_argument_registry[command][dest] = CliCommandArgument(dest, **kwargs) -def cli_command(name, operation, client_factory=None, transform=None): +def cli_command(name, operation, client_factory=None, transform=None, simple_output_query=None): """ Registers a default Azure CLI command. These commands require no special parameters. """ - command_table[name] = create_command(name, operation, transform, client_factory) + command_table[name] = create_command(name, operation, transform, simple_output_query, + client_factory) + +def create_command(name, operation, transform_result, simple_output_query, client_factory): -def create_command(name, operation, transform_result, client_factory): def _execute_command(kwargs): client = client_factory(kwargs) if client_factory else None try: @@ -226,7 +230,7 @@ def _execute_command(kwargs): raise CLIError(message) name = ' '.join(name.split()) - cmd = CliCommand(name, _execute_command) + cmd = CliCommand(name, _execute_command, simple_output_query=simple_output_query) cmd.arguments.update(extract_args_from_signature(operation)) return cmd diff --git a/src/azure/cli/extensions/query.py b/src/azure/cli/extensions/query.py index 40d5d576ddf..bb016b2adf1 100644 --- a/src/azure/cli/extensions/query.py +++ b/src/azure/cli/extensions/query.py @@ -40,6 +40,7 @@ def filter_output(**kwargs): kwargs['event_data']['result'], Options(collections.OrderedDict)) application.remove(application.FILTER_RESULT, filter_output) application.register(application.FILTER_RESULT, filter_output) + application.session['query_active'] = True application.register(application.GLOBAL_PARSER_CREATED, _register_global_parameter) application.register(application.COMMAND_PARSER_PARSED, handle_query_parameter) diff --git a/src/azure/cli/main.py b/src/azure/cli/main.py index da14c424cae..73730030517 100644 --- a/src/azure/cli/main.py +++ b/src/azure/cli/main.py @@ -55,7 +55,7 @@ def main(args, file=sys.stdout): #pylint: disable=redefined-builtin cmd_result = APPLICATION.execute(args) # Commands can return a dictionary/list of results # If they do, we print the results. - if cmd_result: + if cmd_result and cmd_result.result: formatter = OutputProducer.get_formatter(APPLICATION.configuration.output_format) OutputProducer(formatter=formatter, file=file).out(cmd_result) except Exception as ex: # pylint: disable=broad-except diff --git a/src/azure/cli/tests/test_output.py b/src/azure/cli/tests/test_output.py index b756a5235ac..fc7cfac510c 100644 --- a/src/azure/cli/tests/test_output.py +++ b/src/azure/cli/tests/test_output.py @@ -9,7 +9,7 @@ from six import StringIO from azure.cli._output import (OutputProducer, format_json, format_table, format_list, - format_tsv, ListOutput) + format_tsv, ListOutput, CommandResultItem) import azure.cli._util as util class TestOutput(unittest.TestCase): @@ -33,7 +33,7 @@ def test_out_json_valid(self): The JSON output when the input is a dict should be the dict serialized to JSON """ output_producer = OutputProducer(formatter=format_json, file=self.io) - output_producer.out({'active': True, 'id': '0b1f6472'}) + output_producer.out(CommandResultItem({'active': True, 'id': '0b1f6472'})) self.assertEqual(util.normalize_newlines(self.io.getvalue()), util.normalize_newlines( """{ "active": true, @@ -43,7 +43,7 @@ def test_out_json_valid(self): def test_out_json_byte(self): output_producer = OutputProducer(formatter=format_json, file=self.io) - output_producer.out({'active': True, 'contents': b'0b1f6472'}) + output_producer.out(CommandResultItem({'active': True, 'contents': b'0b1f6472'})) self.assertEqual(util.normalize_newlines(self.io.getvalue()), util.normalize_newlines( """{ "active": true, @@ -53,7 +53,7 @@ def test_out_json_byte(self): def test_out_json_byte_empty(self): output_producer = OutputProducer(formatter=format_json, file=self.io) - output_producer.out({'active': True, 'contents': b''}) + output_producer.out(CommandResultItem({'active': True, 'contents': b''})) self.assertEqual(util.normalize_newlines(self.io.getvalue()), util.normalize_newlines( """{ "active": true, @@ -63,22 +63,99 @@ def test_out_json_byte_empty(self): def test_out_boolean_valid(self): output_producer = OutputProducer(formatter=format_list, file=self.io) - output_producer.out(True) + output_producer.out(CommandResultItem(True)) self.assertEqual(util.normalize_newlines(self.io.getvalue()), util.normalize_newlines("""True\n\n\n""")) + # TABLE output tests + def test_out_table_valid(self): output_producer = OutputProducer(formatter=format_table, file=self.io) - output_producer.out({'active': True, 'id': '0b1f6472'}) + output_producer.out(CommandResultItem({'active': True, 'id': '0b1f6472'})) self.assertEqual(util.normalize_newlines(self.io.getvalue()), util.normalize_newlines( """active | id -------|--------- True | 0b1f6472 """)) + def test_out_table_valid_query1(self): + output_producer = OutputProducer(formatter=format_table, file=self.io) + result_item = CommandResultItem([{'name': 'qwerty', 'id': '0b1f6472qwerty'}, + {'name': 'asdf', 'id': '0b1f6472asdf'}], + simple_output_query='[*].{Name:name, Id:id}') + output_producer.out(result_item) + self.assertEqual(util.normalize_newlines(self.io.getvalue()), util.normalize_newlines( +""" Name | Id +-------|--------------- +qwerty | 0b1f6472qwerty +asdf | 0b1f6472asdf +""")) + + def test_out_table_valid_query2(self): + output_producer = OutputProducer(formatter=format_table, file=self.io) + result_item = CommandResultItem([{'name': 'qwerty', 'id': '0b1f6472qwerty'}, + {'name': 'asdf', 'id': '0b1f6472asdf'}], + simple_output_query='[*].{Name:name}') + output_producer.out(result_item) + self.assertEqual(util.normalize_newlines(self.io.getvalue()), util.normalize_newlines( +""" Name +------ +qwerty +asdf +""")) + + def test_out_table_valid_query_but_query_active(self): + """Query has been set but there is an active query so do not apply simple_output_query""" + output_producer = OutputProducer(formatter=format_table, file=self.io) + result_item = CommandResultItem([{'name': 'qwerty', 'id': '0b1f6472qwerty'}, + {'name': 'asdf', 'id': '0b1f6472asdf'}], + simple_output_query='[*].{Name:name}', + is_query_active=True) + output_producer.out(result_item) + self.assertEqual(util.normalize_newlines(self.io.getvalue()), util.normalize_newlines( +""" name | id +-------|--------------- +qwerty | 0b1f6472qwerty +asdf | 0b1f6472asdf +""")) + + def test_out_table_bad_query(self): + output_producer = OutputProducer(formatter=format_table, file=self.io) + result_item = CommandResultItem([{'name': 'qwerty', 'id': '0b1f6472qwerty'}, + {'name': 'asdf', 'id': '0b1f6472asdf'}], + simple_output_query='[*].{Name:name') + with self.assertRaises(util.CLIError): + output_producer.out(result_item) + + def test_out_table_complex_obj(self): + output_producer = OutputProducer(formatter=format_table, file=self.io) + result_item = CommandResultItem([{'name': 'qwerty', 'id': '0b1f6472qwerty', 'sub': {'1'}}]) + with self.assertRaises(util.CLIError): + output_producer.out(result_item) + + def test_out_table_complex_obj_with_query_ok(self): + output_producer = OutputProducer(formatter=format_table, file=self.io) + result_item = CommandResultItem([{'name': 'qwerty', 'id': '0b1f6472qwerty', 'sub': {'1'}}], + simple_output_query='[*].{Name:name}') + output_producer.out(result_item) + self.assertEqual(util.normalize_newlines(self.io.getvalue()), util.normalize_newlines( +""" Name +------ +qwerty +""")) + + def test_out_table_complex_obj_with_query_still_complex(self): + output_producer = OutputProducer(formatter=format_table, file=self.io) + result_item = CommandResultItem([{'name': 'qwerty', 'id': '0b1f6472qwerty', 'sub': {'1'}}], + simple_output_query='[*].{Name:name, Sub:sub}') + with self.assertRaises(util.CLIError): + output_producer.out(result_item) + + # LIST output tests + def test_out_list_valid(self): output_producer = OutputProducer(formatter=format_list, file=self.io) - output_producer.out({'active': True, 'id': '0b1f6472'}) + output_producer.out(CommandResultItem({'active': True, 'id': '0b1f6472'})) self.assertEqual(util.normalize_newlines(self.io.getvalue()), util.normalize_newlines( """Active : True Id : 0b1f6472 @@ -88,7 +165,7 @@ def test_out_list_valid(self): def test_out_list_valid_none_val(self): output_producer = OutputProducer(formatter=format_list, file=self.io) - output_producer.out({'active': None, 'id': '0b1f6472'}) + output_producer.out(CommandResultItem({'active': None, 'id': '0b1f6472'})) self.assertEqual(util.normalize_newlines(self.io.getvalue()), util.normalize_newlines( """Active : None Id : 0b1f6472 @@ -98,7 +175,7 @@ def test_out_list_valid_none_val(self): def test_out_list_valid_empty_array(self): output_producer = OutputProducer(formatter=format_list, file=self.io) - output_producer.out({'active': None, 'id': '0b1f6472', 'hosts': []}) + output_producer.out(CommandResultItem({'active': None, 'id': '0b1f6472', 'hosts': []})) self.assertEqual(util.normalize_newlines(self.io.getvalue()), util.normalize_newlines( """Active : None Id : 0b1f6472 @@ -110,10 +187,10 @@ def test_out_list_valid_empty_array(self): def test_out_list_valid_array_complex(self): output_producer = OutputProducer(formatter=format_list, file=self.io) - output_producer.out([ + output_producer.out(CommandResultItem([ {'active': True, 'id': '783yesdf'}, {'active': False, 'id': '3hjnme32'}, - {'active': False, 'id': '23hiujbs'}]) + {'active': False, 'id': '23hiujbs'}])) self.assertEqual(util.normalize_newlines(self.io.getvalue()), util.normalize_newlines( """Active : True Id : 783yesdf @@ -129,7 +206,7 @@ def test_out_list_valid_array_complex(self): def test_out_list_valid_str_array(self): output_producer = OutputProducer(formatter=format_list, file=self.io) - output_producer.out(['location', 'id', 'host', 'server']) + output_producer.out(CommandResultItem(['location', 'id', 'host', 'server'])) self.assertEqual(util.normalize_newlines(self.io.getvalue()), util.normalize_newlines( """location @@ -144,7 +221,8 @@ def test_out_list_valid_str_array(self): def test_out_list_valid_complex_array(self): output_producer = OutputProducer(formatter=format_list, file=self.io) - output_producer.out({'active': True, 'id': '0b1f6472', 'myarray': ['1', '2', '3', '4']}) + output_producer.out(CommandResultItem({'active': True, 'id': '0b1f6472', + 'myarray': ['1', '2', '3', '4']})) self.assertEqual(util.normalize_newlines(self.io.getvalue()), util.normalize_newlines( """Active : True Id : 0b1f6472 @@ -186,14 +264,14 @@ def test_output_format_dict(self): obj = {} obj['A'] = 1 obj['B'] = 2 - result = format_tsv(obj) + result = format_tsv(CommandResultItem(obj)) self.assertEqual(result, '1\t2\n') def test_output_format_dict_sort(self): obj = {} obj['B'] = 1 obj['A'] = 2 - result = format_tsv(obj) + result = format_tsv(CommandResultItem(obj)) self.assertEqual(result, '2\t1\n') def test_output_format_ordereddict_not_sorted(self): @@ -201,7 +279,7 @@ def test_output_format_ordereddict_not_sorted(self): obj = OrderedDict() obj['B'] = 1 obj['A'] = 2 - result = format_tsv(obj) + result = format_tsv(CommandResultItem(obj)) self.assertEqual(result, '1\t2\n') def test_output_format_ordereddict_list_not_sorted(self): @@ -213,7 +291,7 @@ def test_output_format_ordereddict_list_not_sorted(self): obj2 = OrderedDict() obj2['A'] = 3 obj2['B'] = 4 - result = format_tsv([obj1, obj2]) + result = format_tsv(CommandResultItem([obj1, obj2])) self.assertEqual(result, '1\t2\n3\t4\n') if __name__ == '__main__': diff --git a/src/command_modules/azure-cli-network/azure/cli/command_modules/network/generated.py b/src/command_modules/azure-cli-network/azure/cli/command_modules/network/generated.py index ff82e1da9c1..c5a218eefda 100644 --- a/src/command_modules/azure-cli-network/azure/cli/command_modules/network/generated.py +++ b/src/command_modules/azure-cli-network/azure/cli/command_modules/network/generated.py @@ -206,7 +206,7 @@ factory = lambda _: _network_client_factory().public_ip_addresses cli_command('network public-ip delete', PublicIPAddressesOperations.delete, factory) cli_command('network public-ip show', PublicIPAddressesOperations.get, factory) -cli_command('network public-ip list', list_public_ips) +cli_command('network public-ip list', list_public_ips, simple_output_query="[*].{Name:name, ResourceGroup:resourceGroup, Location:location, IpAddress:ipAddress, FQDN:dnsSettings.fqdn} | sort_by(@, &Name)") register_generic_update('network public-ip update', PublicIPAddressesOperations.get, PublicIPAddressesOperations.create_or_update, factory) factory = lambda _: get_mgmt_service_client(PublicIPClient).public_ip diff --git a/src/command_modules/azure-cli-storage/azure/cli/command_modules/storage/_command_type.py b/src/command_modules/azure-cli-storage/azure/cli/command_modules/storage/_command_type.py index 4844958b510..5db291b45a8 100644 --- a/src/command_modules/azure-cli-storage/azure/cli/command_modules/storage/_command_type.py +++ b/src/command_modules/azure-cli-storage/azure/cli/command_modules/storage/_command_type.py @@ -8,11 +8,11 @@ from azure.cli.command_modules.storage._validators import validate_client_parameters def cli_storage_data_plane_command(name, operation, client_factory, - transform=None): + transform=None, simple_output_query=None): """ Registers an Azure CLI Storage Data Plane command. These commands always include the four parameters which can be used to obtain a storage client: account-name, account-key, connection-string, and sas-token. """ - command = create_command(name, operation, transform, client_factory) + command = create_command(name, operation, transform, simple_output_query, client_factory) # add parameters required to create a storage client command.add_argument('account_name', '--account-name', required=False, default=None) diff --git a/src/command_modules/azure-cli-storage/azure/cli/command_modules/storage/generated.py b/src/command_modules/azure-cli-storage/azure/cli/command_modules/storage/generated.py index 51cf7376ac5..410e9c95dda 100644 --- a/src/command_modules/azure-cli-storage/azure/cli/command_modules/storage/generated.py +++ b/src/command_modules/azure-cli-storage/azure/cli/command_modules/storage/generated.py @@ -12,6 +12,7 @@ from azure.storage import CloudStorageAccount from azure.cli.commands import cli_command + from azure.cli.command_modules.storage._command_type import cli_storage_data_plane_command from azure.cli.command_modules.storage._factory import \ (storage_client_factory, blob_data_service_factory, file_data_service_factory, @@ -29,7 +30,7 @@ cli_command('storage account delete', StorageAccountsOperations.delete, factory) cli_command('storage account show', StorageAccountsOperations.get_properties, factory) cli_command('storage account create', create_storage_account) -cli_command('storage account list', list_storage_accounts) +cli_command('storage account list', list_storage_accounts, simple_output_query='[*].{Name: name, ResourceGroup: resourceGroup, Location: location, SkuName: sku.name, SkuTier: sku.tier} | sort_by(@, &Name)') cli_command('storage account show-usage', show_storage_account_usage) cli_command('storage account update', set_storage_account_properties) cli_command('storage account connection-string', show_storage_account_connection_string) diff --git a/src/command_modules/azure-cli-vm/azure/cli/command_modules/vm/generated.py b/src/command_modules/azure-cli-vm/azure/cli/command_modules/vm/generated.py index 835a58a14ed..12e669a072c 100644 --- a/src/command_modules/azure-cli-vm/azure/cli/command_modules/vm/generated.py +++ b/src/command_modules/azure-cli-vm/azure/cli/command_modules/vm/generated.py @@ -52,14 +52,14 @@ cli_command('vm delete', VirtualMachinesOperations.delete, factory) cli_command('vm deallocate', VirtualMachinesOperations.deallocate, factory) cli_command('vm generalize', VirtualMachinesOperations.generalize, factory) -cli_command('vm show', VirtualMachinesOperations.get, factory) +cli_command('vm show', VirtualMachinesOperations.get, factory, simple_output_query="{Name:name, ResourceGroup:resourceGroup, Location:location, VmSize:hardwareProfile.vmSize, OsType: storageProfile.osDisk.osType, Urn: join(':', [storageProfile.imageReference.publisher, storageProfile.imageReference.offer, storageProfile.imageReference.sku, storageProfile.imageReference.version])}") cli_command('vm list-vm-resize-options', VirtualMachinesOperations.list_available_sizes, factory) cli_command('vm stop', VirtualMachinesOperations.power_off, factory) cli_command('vm restart', VirtualMachinesOperations.restart, factory) cli_command('vm start', VirtualMachinesOperations.start, factory) cli_command('vm redeploy', VirtualMachinesOperations.redeploy, factory) cli_command('vm list-ip-addresses', list_ip_addresses) -cli_command('vm list', list_vm) +cli_command('vm list', list_vm, simple_output_query="[*].{Name: name, ResourceGroup: resourceGroup, Location: location, VmSize: hardwareProfile.vmSize, Urn: join(':', [storageProfile.imageReference.publisher, storageProfile.imageReference.offer, storageProfile.imageReference.sku, storageProfile.imageReference.version])} | sort_by(@, &Name)") cli_command('vm resize', resize_vm) cli_command('vm capture', capture_vm) cli_command('vm nic add', vm_add_nics) diff --git a/src/command_modules/azure-cli-vm/azure/cli/command_modules/vm/tests/test_vm_parameters.py b/src/command_modules/azure-cli-vm/azure/cli/command_modules/vm/tests/test_vm_parameters.py index 6b479e46b60..545d66eb5c5 100644 --- a/src/command_modules/azure-cli-vm/azure/cli/command_modules/vm/tests/test_vm_parameters.py +++ b/src/command_modules/azure-cli-vm/azure/cli/command_modules/vm/tests/test_vm_parameters.py @@ -36,7 +36,7 @@ def test_parse_vm_show(self): 'expand': None, 'resource_group_name': 'thisisaresourcegroup', 'vm_name': 'thisisavmname' - }, args) + }, args.result) # Invalid resource ID should trigger the missing resource group # parameter failure @@ -54,14 +54,14 @@ def test_parse_vm_list(self): args = mock_echo_args('vm list', '') self.assertDictEqual({ 'resource_group_name': None, - }, args) + }, args.result) # if resource group name is specified, however, # it should get passed through... args = mock_echo_args('vm list', '-g hullo') self.assertDictEqual({ 'resource_group_name': 'hullo', - }, args) + }, args.result) consistent_arguments = { 'resource_group_name': ('--resource-group', '-g'), From 26a98ed49f9d95bb04aeecff1c322e5531189205 Mon Sep 17 00:00:00 2001 From: Derek Bekoe Date: Tue, 2 Aug 2016 16:49:44 -0700 Subject: [PATCH 2/3] A query (built-in or custom) is required for table format. Otherwise, the ordering of columns is not guaranteed. --- src/azure/cli/_output.py | 2 ++ src/azure/cli/tests/test_output.py | 29 +++++------------------------ 2 files changed, 7 insertions(+), 24 deletions(-) diff --git a/src/azure/cli/_output.py b/src/azure/cli/_output.py index 4b6791bec09..411e30177eb 100644 --- a/src/azure/cli/_output.py +++ b/src/azure/cli/_output.py @@ -37,6 +37,8 @@ def format_json(obj): def format_table(obj): result = obj.result try: + if not obj.simple_output_query and not obj.is_query_active: + raise ValueError('No query specified and no built-in query available.') if obj.simple_output_query and not obj.is_query_active: from jmespath import compile as compile_jmespath, search, Options result = compile_jmespath(obj.simple_output_query).search(result, Options(OrderedDict)) diff --git a/src/azure/cli/tests/test_output.py b/src/azure/cli/tests/test_output.py index fc7cfac510c..47b0acdb5e4 100644 --- a/src/azure/cli/tests/test_output.py +++ b/src/azure/cli/tests/test_output.py @@ -69,15 +69,6 @@ def test_out_boolean_valid(self): # TABLE output tests - def test_out_table_valid(self): - output_producer = OutputProducer(formatter=format_table, file=self.io) - output_producer.out(CommandResultItem({'active': True, 'id': '0b1f6472'})) - self.assertEqual(util.normalize_newlines(self.io.getvalue()), util.normalize_newlines( -"""active | id --------|--------- -True | 0b1f6472 -""")) - def test_out_table_valid_query1(self): output_producer = OutputProducer(formatter=format_table, file=self.io) result_item = CommandResultItem([{'name': 'qwerty', 'id': '0b1f6472qwerty'}, @@ -91,6 +82,11 @@ def test_out_table_valid_query1(self): asdf | 0b1f6472asdf """)) + def test_out_table_no_query(self): + output_producer = OutputProducer(formatter=format_table, file=self.io) + with self.assertRaises(util.CLIError): + output_producer.out(CommandResultItem({'active': True, 'id': '0b1f6472'})) + def test_out_table_valid_query2(self): output_producer = OutputProducer(formatter=format_table, file=self.io) result_item = CommandResultItem([{'name': 'qwerty', 'id': '0b1f6472qwerty'}, @@ -102,21 +98,6 @@ def test_out_table_valid_query2(self): ------ qwerty asdf -""")) - - def test_out_table_valid_query_but_query_active(self): - """Query has been set but there is an active query so do not apply simple_output_query""" - output_producer = OutputProducer(formatter=format_table, file=self.io) - result_item = CommandResultItem([{'name': 'qwerty', 'id': '0b1f6472qwerty'}, - {'name': 'asdf', 'id': '0b1f6472asdf'}], - simple_output_query='[*].{Name:name}', - is_query_active=True) - output_producer.out(result_item) - self.assertEqual(util.normalize_newlines(self.io.getvalue()), util.normalize_newlines( -""" name | id --------|--------------- -qwerty | 0b1f6472qwerty -asdf | 0b1f6472asdf """)) def test_out_table_bad_query(self): From 175849453c8af80a44ae2a9231cd64260a5fec86 Mon Sep 17 00:00:00 2001 From: Derek Bekoe Date: Wed, 3 Aug 2016 09:54:11 -0700 Subject: [PATCH 3/3] Run bash on container start --- Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index daec1352850..ab9ce338641 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,4 +39,6 @@ complete -o nospace -F _python_argcomplete \"az\"\n\ " > /etc/az.completion RUN echo "\nsource '/etc/az.completion'\n" >> /etc/bash.bashrc -CMD az +WORKDIR / + +CMD az; bash