diff --git a/Dockerfile b/Dockerfile index 204659f5c05..20df86a2157 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,36 +1,26 @@ -FROM ubuntu:14.04 - -RUN rm /bin/sh && ln -s /bin/bash /bin/sh +FROM ubuntu:15.10 RUN apt-get update -qq && \ apt-get install -qqy --no-install-recommends\ - build-essential \ - curl \ - ca-certificates \ - git \ - python-pip \ - libffi-dev \ - libssl-dev \ - python-dev \ + python3-pip \ vim \ - nano \ jq && \ - rm -rf /var/lib/apt/lists/* && \ - pip install azure==2.0.0a1 && \ - pip install --upgrade requests && \ - pip install cryptography && \ - pip install pyopenssl ndg-httpsclient pyasn1 + rm -rf /var/lib/apt/lists/* + +RUN pip3 install azure==2.0.0rc1 -ENV AZURECLITEMP /tmp/azure-cli +ENV AZURECLITEMP /opt/azure-cli ENV PYTHONPATH $PYTHONPATH:$AZURECLITEMP/src ENV PATH $PATH:$AZURECLITEMP RUN mkdir -p $AZURECLITEMP -ADD src $AZURECLITEMP/src +COPY src $AZURECLITEMP/src +COPY az.completion.sh $AZURECLITEMP/ +COPY az $AZURECLITEMP/ -RUN echo '#!/bin/bash'>$AZURECLITEMP/az && \ - echo 'python -m azure.cli "$@"'>>$AZURECLITEMP/az && \ - chmod +x $AZURECLITEMP/az && \ - az +RUN chmod +x $AZURECLITEMP/az +RUN ln /usr/bin/python3 /usr/bin/python +RUN echo "source $AZURECLITEMP/az.completion.sh" >> ~/.bashrc +RUN az -ENV EDITOR vim +ENV EDITOR vim \ No newline at end of file diff --git a/Dockerfile-2.7 b/Dockerfile-2.7 new file mode 100644 index 00000000000..8ceae9011ce --- /dev/null +++ b/Dockerfile-2.7 @@ -0,0 +1,33 @@ +FROM ubuntu:14.04 + +RUN apt-get update -qq && \ + apt-get install -qqy --no-install-recommends\ + build-essential \ + curl \ + ca-certificates \ + python-pip \ + libffi-dev \ + libssl-dev \ + python-dev \ + vim \ + jq && \ + rm -rf /var/lib/apt/lists/* && \ + pip install azure==2.0.0rc1 && \ + pip install --upgrade requests && \ + pip install cryptography && \ + pip install pyopenssl ndg-httpsclient pyasn1 + +ENV AZURECLITEMP /opt/azure-cli +ENV PYTHONPATH $PYTHONPATH:$AZURECLITEMP/src +ENV PATH $PATH:$AZURECLITEMP + +RUN mkdir -p $AZURECLITEMP +COPY src $AZURECLITEMP/src +COPY az.completion.sh $AZURECLITEMP/ +COPY az $AZURECLITEMP/ + +RUN chmod +x $AZURECLITEMP/az +RUN echo "source $AZURECLITEMP/az.completion.sh" >> ~/.bashrc +RUN az + +ENV EDITOR vim \ No newline at end of file diff --git a/az b/az index c4b5a8d6d7b..e7b0cefdac4 100644 --- a/az +++ b/az @@ -10,6 +10,5 @@ DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" export PYTHONPATH="${DIR}/src:${PYTHONPATH}" -python -m azure.cli "$@" - +python -m azure.cli "$@" diff --git a/az.completion.sh b/az.completion.sh new file mode 100644 index 00000000000..654b52eb0d2 --- /dev/null +++ b/az.completion.sh @@ -0,0 +1,13 @@ +if type compdef &>/dev/null; then + #ZSH + _az_complete() { + compadd -- `"${COMP_WORDS[0]}" --complete "${words[@]:1}"` + } + compdef _az_complete az +elif type complete &>/dev/null; then + #BASH + _az_complete() { + COMPREPLY=( $(compgen -W '$("${COMP_WORDS[0]}" --complete "${COMP_WORDS[@]:1}")' -- "${COMP_WORDS[COMP_CWORD]}") ) + } + complete -F _az_complete az +fi diff --git a/azure-cli.pyproj b/azure-cli.pyproj index f029358170c..28a4caa8398 100644 --- a/azure-cli.pyproj +++ b/azure-cli.pyproj @@ -12,7 +12,7 @@ . {888888a0-9f3d-457c-b088-3a5042f75d52} Standard Python launcher - {4ae3497d-f45c-4ec2-9e26-57476ef276a0} + {1dd9c42b-5980-42ce-a2c3-46d3bf0eede4} 3.5 @@ -25,7 +25,12 @@ + + + + + @@ -54,28 +59,6 @@ - - {83c20e12-84e3-4c10-a1ba-466d2b57483d} - {2af0f10d-7135-4994-9156-5d01c9c11b7e} - 2.7 - env27b (Python 2.7) - Scripts\python.exe - Scripts\pythonw.exe - Lib\ - PYTHONPATH - X86 - - - {4ae3497d-f45c-4ec2-9e26-57476ef276a0} - {2af0f10d-7135-4994-9156-5d01c9c11b7e} - 3.5 - env35d (Python 3.5) - Scripts\python.exe - Scripts\pythonw.exe - Lib\ - PYTHONPATH - X86 - {1dd9c42b-5980-42ce-a2c3-46d3bf0eede4} {2af0f10d-7135-4994-9156-5d01c9c11b7e} diff --git a/requirements.txt b/requirements.txt index 7f63073e6f3..558ba24df39 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -azure==2.0.0a1 +azure==2.0.0rc1 mock==1.3.0 pylint==1.5.4 six==1.10.0 diff --git a/src/azure/cli/_argparse.py b/src/azure/cli/_argparse.py index fc7ae204ed6..ab825f2045b 100644 --- a/src/azure/cli/_argparse.py +++ b/src/azure/cli/_argparse.py @@ -3,6 +3,7 @@ from ._locale import L, get_file as locale_get_file from ._logging import logger +from ._output import OutputProducer # Named arguments are prefixed with one of these strings ARG_PREFIXES = sorted(('-', '--', '/'), key=len, reverse=True) @@ -57,6 +58,10 @@ def _index(string, char, default=sys.maxsize): except ValueError: return default +class ArgumentParserResult(object): #pylint: disable=too-few-public-methods + def __init__(self, result, output_format=None): + self.result = result + self.output_format = output_format class ArgumentParser(object): def __init__(self, prog): @@ -125,6 +130,7 @@ def add_command(self, #pylint: disable=too-many-branches #pylint: disable=too-many-statements #pylint: disable=too-many-locals + #pylint: disable=too-many-return-statements def execute(self, args, show_usage=False, @@ -177,9 +183,9 @@ def not_global(a): show_usage = True if show_completions: - return self._display_completions(m, args, out) + return ArgumentParserResult(self._display_completions(m, args, out)) if show_usage: - return self._display_usage(nouns, m, out) + return ArgumentParserResult(self._display_usage(nouns, m, out)) parsed = Arguments() others = Arguments() @@ -199,7 +205,7 @@ def not_global(a): if value is not None: print(L("argument '{0}' does not take a value").format(key_n), file=out) - return self._display_usage(nouns, m, out) + return ArgumentParserResult(self._display_usage(nouns, m, out)) parsed.add_from_dotted(target_value[0], True) else: # Arg with a value @@ -217,15 +223,24 @@ def not_global(a): parsed[a] except KeyError: print(L("Missing required argument {}".format(a))) - return self._display_usage(nouns, m, out) + return ArgumentParserResult(self._display_usage(nouns, m, out)) + + output_format = None + if others and 'output' in others: + if others['output'] in OutputProducer.format_dict: + output_format = others['output'] + del others['output'] + else: + print(L("Invalid output format '{}' specified".format(others['output']))) + return ArgumentParserResult(self._display_usage(nouns, m, out)) old_stdout = sys.stdout try: sys.stdout = out - return handler(parsed, others) + return ArgumentParserResult(handler(parsed, others), output_format) except IncorrectUsageError as ex: print(str(ex), file=out) - return self._display_usage(nouns, m, out) + return ArgumentParserResult(self._display_usage(nouns, m, out)) finally: sys.stdout = old_stdout @@ -264,15 +279,16 @@ def _display_usage(self, nouns, noun_map, out=sys.stdout): out.flush() logger.debug('Expected documentation at %s', doc_file) - def _display_completions(self, noun_map, arguments, out=sys.stdout): # pylint: disable=no-self-use - arguments.remove('--complete') + def _display_completions(self, noun_map, arguments, out=sys.stdout): + for a in self.complete_args: + arguments.remove(a) command_candidates = set([k for k in noun_map if not k.startswith('$')]) if command_candidates and not arguments[-1].startswith('-'): command_candidates = set([c for c in command_candidates if c.startswith(arguments[-1])]) kwargs = noun_map.get('$kwargs') or [] - args_candidates = set('--' + a for a in kwargs if a) + args_candidates = set(('--' if len(a) > 1 else '-') + a for a in kwargs) if arguments[-1].startswith('-'): # TODO: We don't have enough metadata about the command to do parameter value # completion (yet). This should only apply to value arguments, not flag arguments diff --git a/src/azure/cli/_output.py b/src/azure/cli/_output.py index f5dc596f7e0..88acb27eb49 100644 --- a/src/azure/cli/_output.py +++ b/src/azure/cli/_output.py @@ -2,13 +2,9 @@ import sys import json +import re -try: - # Python 3 - from io import StringIO -except ImportError: - # Python 2 - from StringIO import StringIO #pylint: disable=import-error +from six import StringIO class OutputFormatException(Exception): pass @@ -40,8 +36,20 @@ def format_text(obj): except TypeError: return '' +def format_list(obj): + obj_list = obj if isinstance(obj, list) else [obj] + lo = ListOutput() + return lo.dump(obj_list) + class OutputProducer(object): #pylint: disable=too-few-public-methods + format_dict = { + 'json': format_json, + 'table': format_table, + 'text': format_text, + 'list': format_list + } + def __init__(self, formatter=format_json, file=sys.stdout): #pylint: disable=redefined-builtin self.formatter = formatter self.file = file @@ -49,6 +57,84 @@ def __init__(self, formatter=format_json, file=sys.stdout): #pylint: disable=red def out(self, obj): print(self.formatter(obj), file=self.file) + @staticmethod + def get_formatter(format_type): + return OutputProducer.format_dict.get(format_type, format_list) + +class ListOutput(object): #pylint: disable=too-few-public-methods + + # Match the capital letters in a camel case string + FORMAT_KEYS_PATTERN = re.compile('([A-Z][^A-Z]*)') + + def __init__(self): + self._formatted_keys_cache = {} + + @staticmethod + def _get_max_key_len(keys): + return len(max(keys, key=len)) if keys else 0 + + @staticmethod + def _sort_key_func(key, item): + # We want dictionaries to be last so use ASCII char 126 ~ to + # prefix dictionary and list key names. + if isinstance(item[key], dict): + return '~~'+key + elif isinstance(item[key], list): + return '~'+key + else: + return key + + def _get_formatted_key(self, key): + def _format_key(key): + words = [word for word in re.split(ListOutput.FORMAT_KEYS_PATTERN, key) if word] + return ' '.join(words).title() + + try: + return self._formatted_keys_cache[key] + except KeyError: + self._formatted_keys_cache[key] = _format_key(key) + return self._formatted_keys_cache[key] + + @staticmethod + def _dump_line(io, line, indent): + io.write(' ' * indent) + io.write(line) + io.write('\n') + + def _dump_object(self, io, obj, indent): + if isinstance(obj, list): + for array_item in obj: + self._dump_object(io, array_item, indent) + elif isinstance(obj, dict): + # Get the formatted keys for this item + # Skip dicts/lists because those will be handled recursively later. + # We use this object to calc key width and don't want to dicts/lists in this. + obj_fk = {k: self._get_formatted_key(k) + for k in obj if not isinstance(obj[k], dict) and not isinstance(obj[k], list)} + key_width = ListOutput._get_max_key_len(obj_fk.values()) + for key in sorted(obj, key=lambda x: ListOutput._sort_key_func(x, obj)): + if isinstance(obj[key], dict) or isinstance(obj[key], list): + # complex object + io.write('\n') + ListOutput._dump_line(io, self._get_formatted_key(key).upper(), indent+1) + self._dump_object(io, obj[key] if obj[key] else 'None', indent+1) + else: + # non-complex so write it + line = '%s : %s' % (self._get_formatted_key(key).ljust(key_width), + 'None' if obj[key] is None else obj[key]) + ListOutput._dump_line(io, line, indent) + else: + ListOutput._dump_line(io, obj, indent) + + def dump(self, data): + io = StringIO() + for obj in data: + self._dump_object(io, obj, 0) + io.write('\n') + result = io.getvalue() + io.close() + return result + class TableOutput(object): def __init__(self): self._rows = [{}] @@ -59,16 +145,18 @@ def dump(self): if len(self._rows) == 1: return - with StringIO() as io: - cols = [(c, self._columns[c]) for c in self._column_order] - io.write(' | '.join(c.center(w) for c, w in cols)) - io.write('\n') - io.write('-|-'.join('-' * w for c, w in cols)) + io = StringIO() + cols = [(c, self._columns[c]) for c in self._column_order] + io.write(' | '.join(c.center(w) for c, w in cols)) + io.write('\n') + io.write('-|-'.join('-' * w for c, w in cols)) + io.write('\n') + for r in self._rows[:-1]: + io.write(' | '.join(r[c].ljust(w) for c, w in cols)) io.write('\n') - for r in self._rows[:-1]: - io.write(' | '.join(r[c].ljust(w) for c, w in cols)) - io.write('\n') - return io.getvalue() + result = io.getvalue() + io.close() + return result @property def any_rows(self): @@ -99,17 +187,19 @@ def add(self, identifier, value): self.identifiers[identifier] = [value] def dump(self): - with StringIO() as io: - for identifier in sorted(self.identifiers): - io.write(identifier.upper()) + io = StringIO() + for identifier in sorted(self.identifiers): + io.write(identifier.upper()) + io.write('\t') + for col in self.identifiers[identifier]: + if isinstance(col, str): + io.write(col) + else: + # TODO: Need to handle complex objects + io.write("null") io.write('\t') - for col in self.identifiers[identifier]: - if isinstance(col, str): - io.write(col) - else: - # TODO: Need to handle complex objects - io.write("null") - io.write('\t') - io.write('\n') - return io.getvalue() + io.write('\n') + result = io.getvalue() + io.close() + return result diff --git a/src/azure/cli/commands/_auto_command.py b/src/azure/cli/commands/_auto_command.py index 02a2722b190..821aa4d1fe6 100644 --- a/src/azure/cli/commands/_auto_command.py +++ b/src/azure/cli/commands/_auto_command.py @@ -23,7 +23,8 @@ def __call__(self, poller): try: while not poller.done(): if self.progress_file: - print('.', end='', flush=True, file=self.progress_file) + print('.', end='', file=self.progress_file) + self.progress_file.flush() time.sleep(self.poll_interval_ms / 1000.0) result = poller.result() succeeded = True diff --git a/src/azure/cli/commands/network.py b/src/azure/cli/commands/network.py index 39f0615eb68..b7a201e9a0b 100644 --- a/src/azure/cli/commands/network.py +++ b/src/azure/cli/commands/network.py @@ -1,3 +1,4 @@ +from msrest import Serializer from .._locale import L from azure.mgmt.network import NetworkManagementClient, NetworkManagementClientConfiguration from azure.mgmt.network.operations import (ApplicationGatewaysOperations, @@ -21,6 +22,7 @@ from ._command_creation import get_service_client from ..commands._auto_command import build_operation, LongRunningOperation +from ..commands import command, description, option def _network_client_factory(): return get_service_client(NetworkManagementClient, NetworkManagementClientConfiguration) @@ -32,12 +34,12 @@ def _network_client_factory(): "application_gateways", _network_client_factory, [ - (ApplicationGatewaysOperations.delete, None), + (ApplicationGatewaysOperations.delete, LongRunningOperation(L('Deleting application gateway'), L('Application gateway deleted'))), (ApplicationGatewaysOperations.get, 'ApplicationGateway'), (ApplicationGatewaysOperations.list, '[ApplicationGateway]'), (ApplicationGatewaysOperations.list_all, '[ApplicationGateway]'), - (ApplicationGatewaysOperations.start, None), - (ApplicationGatewaysOperations.stop, LongRunningOperation(L('Starting application gateway'), L('Application gateway started'))), + (ApplicationGatewaysOperations.start, LongRunningOperation(L('Starting application gateway'), L('Application gateway started'))), + (ApplicationGatewaysOperations.stop, LongRunningOperation(L('Stopping application gateway'), L('Application gateway stopped'))), ]) # ExpressRouteCircuitAuthorizationsOperations @@ -46,7 +48,7 @@ def _network_client_factory(): "express_route_circuit_authorizations", _network_client_factory, [ - (ExpressRouteCircuitAuthorizationsOperations.delete, None), + (ExpressRouteCircuitAuthorizationsOperations.delete, LongRunningOperation(L('Deleting express route authorization'), L('Express route authorization deleted'))), (ExpressRouteCircuitAuthorizationsOperations.get, 'ExpressRouteCircuitAuthorization'), (ExpressRouteCircuitAuthorizationsOperations.list, '[ExpressRouteCircuitAuthorization]'), ]) @@ -57,7 +59,7 @@ def _network_client_factory(): "express_route_circuit_peerings", _network_client_factory, [ - (ExpressRouteCircuitPeeringsOperations.delete, None), + (ExpressRouteCircuitPeeringsOperations.delete, LongRunningOperation(L('Deleting express route circuit peering'), L('Express route circuit peering deleted'))), (ExpressRouteCircuitPeeringsOperations.get, 'ExpressRouteCircuitPeering'), (ExpressRouteCircuitPeeringsOperations.list, '[ExpressRouteCircuitPeering]'), ]) @@ -68,7 +70,7 @@ def _network_client_factory(): "express_route_circuits", _network_client_factory, [ - (ExpressRouteCircuitsOperations.delete, None), + (ExpressRouteCircuitsOperations.delete, LongRunningOperation(L('Deleting express route circuit'), L('Express route circuit deleted'))), (ExpressRouteCircuitsOperations.get, 'ExpressRouteCircuit'), (ExpressRouteCircuitsOperations.list_arp_table, '[ExpressRouteCircuitArpTable]'), (ExpressRouteCircuitsOperations.list_routes_table, '[ExpressRouteCircuitRoutesTable]'), @@ -92,7 +94,7 @@ def _network_client_factory(): "load_balancers", _network_client_factory, [ - (LoadBalancersOperations.delete, None), + (LoadBalancersOperations.delete, LongRunningOperation(L('Deleting load balancer'), L('Load balancer deleted'))), (LoadBalancersOperations.get, 'LoadBalancer'), (LoadBalancersOperations.list_all, '[LoadBalancer]'), (LoadBalancersOperations.list, '[LoadBalancer]'), @@ -105,7 +107,7 @@ def _network_client_factory(): _network_client_factory, [ (LocalNetworkGatewaysOperations.get, 'LocalNetworkGateway'), - (LocalNetworkGatewaysOperations.delete, None), + (LocalNetworkGatewaysOperations.delete, LongRunningOperation(L('Deleting local network gateway'), L('Local network gateway deleted'))), (LocalNetworkGatewaysOperations.list, '[LocalNetworkGateway]'), ]) @@ -116,7 +118,7 @@ def _network_client_factory(): "network_interfaces", _network_client_factory, [ - (NetworkInterfacesOperations.delete, None), + (NetworkInterfacesOperations.delete, LongRunningOperation(L('Deleting network interface'), L('Network interface deleted'))), (NetworkInterfacesOperations.get, 'NetworkInterface'), (NetworkInterfacesOperations.list_virtual_machine_scale_set_vm_network_interfaces, '[NetworkInterface]'), (NetworkInterfacesOperations.list_virtual_machine_scale_set_network_interfaces, '[NetworkInterface]'), @@ -131,7 +133,7 @@ def _network_client_factory(): "network_security_groups", _network_client_factory, [ - (NetworkSecurityGroupsOperations.delete, None), + (NetworkSecurityGroupsOperations.delete, LongRunningOperation(L('Deleting network security group'), L('Network security group deleted'))), (NetworkSecurityGroupsOperations.delete, 'NetworkSecurityGroup'), (NetworkSecurityGroupsOperations.list_all, '[NetworkSecurityGroup]'), (NetworkSecurityGroupsOperations.list, '[NetworkSecurityGroup]'), @@ -143,7 +145,7 @@ def _network_client_factory(): "public_ip_addresses", _network_client_factory, [ - (PublicIPAddressesOperations.delete, None), + (PublicIPAddressesOperations.delete, LongRunningOperation(L('Deleting public IP address'), L('Public IP address deleted'))), (PublicIPAddressesOperations.get, 'PublicIPAddress'), (PublicIPAddressesOperations.list_all, '[PublicIPAddress]'), (PublicIPAddressesOperations.list, '[PublicIPAddress]'), @@ -155,7 +157,7 @@ def _network_client_factory(): "route_tables", _network_client_factory, [ - (RouteTablesOperations.delete, None), + (RouteTablesOperations.delete, LongRunningOperation(L('Deleting route table'), L('Route table deleted'))), (RouteTablesOperations.get, 'RouteTable'), (RouteTablesOperations.list, '[RouteTable]'), (RouteTablesOperations.list_all, '[RouteTable]'), @@ -167,7 +169,7 @@ def _network_client_factory(): "routes", _network_client_factory, [ - (RoutesOperations.delete, None), + (RoutesOperations.delete, LongRunningOperation(L('Deleting route'), L('Route deleted'))), (RoutesOperations.get, 'Route'), (RoutesOperations.list, '[Route]'), ]) @@ -178,7 +180,7 @@ def _network_client_factory(): "security_rules", _network_client_factory, [ - (SecurityRulesOperations.delete, None), + (SecurityRulesOperations.delete, LongRunningOperation(L('Deleting security rule'), L('Security rule deleted'))), (SecurityRulesOperations.get, 'SecurityRule'), (SecurityRulesOperations.list, '[SecurityRule]'), ]) @@ -189,7 +191,7 @@ def _network_client_factory(): "subnets", _network_client_factory, [ - (SubnetsOperations.delete, None), + (SubnetsOperations.delete, LongRunningOperation(L('Deleting subnet'), L('Subnet deleted'))), (SubnetsOperations.get, 'Subnet'), (SubnetsOperations.list, '[Subnet]'), ]) @@ -209,7 +211,7 @@ def _network_client_factory(): "virtual_network_gateway_connections", _network_client_factory, [ - (VirtualNetworkGatewayConnectionsOperations.delete, None), + (VirtualNetworkGatewayConnectionsOperations.delete, LongRunningOperation(L('Deleting virtual network gateway connection'), L('Virtual network gateway connection deleted'))), (VirtualNetworkGatewayConnectionsOperations.get, 'VirtualNetworkGatewayConnection'), (VirtualNetworkGatewayConnectionsOperations.get_shared_key, 'ConnectionSharedKeyResult'), (VirtualNetworkGatewayConnectionsOperations.list, '[VirtualNetworkGatewayConnection]'), @@ -223,7 +225,7 @@ def _network_client_factory(): "virtual_network_gateways", _network_client_factory, [ - (VirtualNetworkGatewaysOperations.delete, None), + (VirtualNetworkGatewaysOperations.delete, LongRunningOperation(L('Deleting virtual network gateway'), L('Virtual network gateway deleted'))), (VirtualNetworkGatewaysOperations.get, 'VirtualNetworkGateway'), (VirtualNetworkGatewaysOperations.list, '[VirtualNetworkGateway]'), (VirtualNetworkGatewaysOperations.reset, 'VirtualNetworkGateway'), @@ -235,8 +237,79 @@ def _network_client_factory(): "virtual_networks", _network_client_factory, [ - (VirtualNetworksOperations.delete, None), + (VirtualNetworksOperations.delete, LongRunningOperation(L('Deleting virtual network'), L('Virtual network deleted'))), (VirtualNetworksOperations.get, 'VirtualNetwork'), (VirtualNetworksOperations.list, '[VirtualNetwork]'), (VirtualNetworksOperations.list_all, '[VirtualNetwork]'), ]) + +@command('network vnet create') +@description(L('Create or update a virtual network (VNet)')) +@option('--resource-group -g ', L('the resource group name'), required=True) +@option('--name -n ', L('the VNet name'), required=True) +@option('--location -l ', L('the VNet location'), required=True) +@option('--address-space -a ', L('the VNet address-space in CIDR notation or multiple address-spaces, quoted and space-seperated'), required=True) +@option('--dns-servers -d ', L('the VNet DNS servers, quoted and space-seperated')) +def create_update_vnet(args, unexpected): #pylint: disable=unused-argument + from azure.mgmt.network.models import AddressSpace, DhcpOptions, VirtualNetwork + + resource_group = args.get('resource-group') + name = args.get('name') + location = args.get('location') + address_space = AddressSpace(address_prefixes=args.get('address-space').split()) + dhcp_options = DhcpOptions(dns_servers=args.get('dns-servers').split()) + + vnet_settings = VirtualNetwork(location=location, + address_space=address_space, + dhcp_options=dhcp_options) + + op = LongRunningOperation('Creating virtual network', 'Virtual network created') + smc = _network_client_factory() + poller = smc.virtual_networks.create_or_update(resource_group, name, vnet_settings) + return Serializer().serialize_data(op(poller), 'VirtualNetwork') + +@command('network subnet create') +@description(L('Create or update a virtual network (VNet) subnet')) +@option('--resource-group -g ', L('the the resource group name'), required=True) +@option('--name -n ', L('the the subnet name'), required=True) +@option('--vnet -v ', L('the name of the subnet vnet'), required=True) +@option('--address-prefix -a ', L('the the address prefix in CIDR format'), required=True) +# TODO: setting the IPConfiguration fails, will contact owning team +#@option('--ip-name -ipn ', L('the IP address configuration name')) +#@option('--ip-private-address -ippa ', L('the private IP address')) +#@option('--ip-allocation-method -ipam ', L('the IP address allocation method')) +#@option('--ip-public-address -ipa ', L('the public IP address')) +def create_update_subnet(args, unexpected): #pylint: disable=unused-argument + from azure.mgmt.network.models import (Subnet, + # TODO: setting the IPConfiguration fails + #IPConfiguration, + ) + + resource_group = args.get('resource-group') + vnet = args.get('vnet') + name = args.get('name') + address_prefix = args.get('address-prefix') + # TODO: setting the IPConfiguration fails, will contact owning team + #ip_name = args.get('ip-name') + #ip_private_address = args.get('ip-private-address') + #ip_allocation_method = args.get('ip-allocation-method') + #ip_public_address = args.get('ip-public-address') + + # TODO: setting the IPConfiguration fails, will contact owning team + #ip_configuration = IPConfiguration(subnet = name, + # name = ip_name, + # private_ip_address = ip_private_address, + # private_ip_allocation_method = ip_allocation_method, + # public_ip_address = ip_public_address) + + subnet_settings = Subnet(name=name, + address_prefix=address_prefix) + # TODO: setting the IPConfiguration fails, will contact owning team + #ip_configurations = [ip_configuration]) + + op = LongRunningOperation('Creating subnet', 'Subnet created') + smc = _network_client_factory() + poller = smc.subnets.create_or_update(resource_group, vnet, name, subnet_settings) + return Serializer().serialize_data(op(poller), 'Subnet') + + diff --git a/src/azure/cli/commands/vm.py b/src/azure/cli/commands/vm.py index 28f2f077c42..7b9ca346485 100644 --- a/src/azure/cli/commands/vm.py +++ b/src/azure/cli/commands/vm.py @@ -44,7 +44,7 @@ def _compute_client_factory(): "virtual_machine_extensions", _compute_client_factory, [ - (VirtualMachineExtensionsOperations.delete, None), + (VirtualMachineExtensionsOperations.delete, LongRunningOperation(L('Deleting VM extension'), L('VM extension deleted'))), (VirtualMachineExtensionsOperations.get, 'VirtualMachineExtension'), ]) @@ -81,14 +81,14 @@ def _compute_client_factory(): "virtual_machines", _compute_client_factory, [ - (VirtualMachinesOperations.delete, None), - (VirtualMachinesOperations.deallocate, None), + (VirtualMachinesOperations.delete, LongRunningOperation(L('Deleting VM'), L('VM Deleted'))), + (VirtualMachinesOperations.deallocate, LongRunningOperation(L('Deallocating VM'), L('VM Deallocated'))), (VirtualMachinesOperations.generalize, None), (VirtualMachinesOperations.get, 'VirtualMachine'), (VirtualMachinesOperations.list, '[VirtualMachine]'), (VirtualMachinesOperations.list_all, '[VirtualMachine]'), (VirtualMachinesOperations.list_available_sizes, '[VirtualMachineSize]'), - (VirtualMachinesOperations.power_off, None), + (VirtualMachinesOperations.power_off, LongRunningOperation(L('Powering off VM'), L('VM powered off'))), (VirtualMachinesOperations.restart, LongRunningOperation(L('Restarting VM'), L('VM Restarted'))), (VirtualMachinesOperations.start, LongRunningOperation(L('Starting VM'), L('VM Started'))), ]) @@ -98,18 +98,18 @@ def _compute_client_factory(): "virtual_machine_scale_sets", _compute_client_factory, [ - (VirtualMachineScaleSetsOperations.deallocate, None), - (VirtualMachineScaleSetsOperations.delete, None), + (VirtualMachineScaleSetsOperations.deallocate, LongRunningOperation(L('Deallocating VM scale set'), L('VM scale set deallocated'))), + (VirtualMachineScaleSetsOperations.delete, LongRunningOperation(L('Deleting VM scale set'), L('VM scale set deleted'))), (VirtualMachineScaleSetsOperations.get, 'VirtualMachineScaleSet'), - (VirtualMachineScaleSetsOperations.delete_instances, None), + (VirtualMachineScaleSetsOperations.delete_instances, LongRunningOperation(L('Deleting VM scale set instances'), L('VM scale set instances deleted'))), (VirtualMachineScaleSetsOperations.get_instance_view, 'VirtualMachineScaleSetInstanceView'), (VirtualMachineScaleSetsOperations.list, '[VirtualMachineScaleSet]'), (VirtualMachineScaleSetsOperations.list_all, '[VirtualMachineScaleSet]'), (VirtualMachineScaleSetsOperations.list_skus, '[VirtualMachineScaleSet]'), - (VirtualMachineScaleSetsOperations.power_off, None), - (VirtualMachineScaleSetsOperations.restart, None), - (VirtualMachineScaleSetsOperations.start, None), - (VirtualMachineScaleSetsOperations.update_instances, None), + (VirtualMachineScaleSetsOperations.power_off, LongRunningOperation(L('Powering off VM scale set'), L('VM scale set powered off'))), + (VirtualMachineScaleSetsOperations.restart, LongRunningOperation(L('Restarting VM scale set'), L('VM scale set restarted'))), + (VirtualMachineScaleSetsOperations.start, LongRunningOperation(L('Starting VM scale set'), L('VM scale set started'))), + (VirtualMachineScaleSetsOperations.update_instances, LongRunningOperation(L('Updating VM scale set instances'), L('VM scale set instances updated'))), ]) build_operation("vm", @@ -117,12 +117,12 @@ def _compute_client_factory(): "virtual_machine_scale_set_vms", _compute_client_factory, [ - (VirtualMachineScaleSetVMsOperations.deallocate, None), - (VirtualMachineScaleSetVMsOperations.delete, None), - (VirtualMachineScaleSetVMsOperations.get, None), + (VirtualMachineScaleSetVMsOperations.deallocate, LongRunningOperation(L('Deallocating VM scale set VMs'), L('VM scale set VMs deallocated'))), + (VirtualMachineScaleSetVMsOperations.delete, LongRunningOperation(L('Deleting VM scale set VMs'), L('VM scale set VMs deleted'))), + (VirtualMachineScaleSetVMsOperations.get, 'VirtualMachineScaleSetVM'), (VirtualMachineScaleSetVMsOperations.get_instance_view, 'VirtualMachineScaleSetVMInstanceView'), (VirtualMachineScaleSetVMsOperations.list, '[VirtualMachineScaleSetVM]'), - (VirtualMachineScaleSetVMsOperations.power_off, None), - (VirtualMachineScaleSetVMsOperations.restart, None), - (VirtualMachineScaleSetVMsOperations.start, None), + (VirtualMachineScaleSetVMsOperations.power_off, LongRunningOperation(L('Powering off VM scale set VMs'), L('VM scale set VMs powered off'))), + (VirtualMachineScaleSetVMsOperations.restart, LongRunningOperation(L('Restarting VM scale set VMs'), L('VM scale set VMs restarted'))), + (VirtualMachineScaleSetVMsOperations.start, LongRunningOperation(L('Starting VM scale set VMs'), L('VM scale set VMs started'))), ]) diff --git a/src/azure/cli/main.py b/src/azure/cli/main.py index a6f20cfa0a8..a12faebfeba 100644 --- a/src/azure/cli/main.py +++ b/src/azure/cli/main.py @@ -39,11 +39,12 @@ def main(args, file=sys.stdout): #pylint: disable=redefined-builtin commands.add_to_parser(parser) try: - result = parser.execute(args) + cmd_res = parser.execute(args) # Commands can return a dictionary/list of results # If they do, we print the results. - if result: - OutputProducer(file=file).out(result) + if cmd_res.result: + formatter = OutputProducer.get_formatter(cmd_res.output_format) + OutputProducer(formatter=formatter, file=file).out(cmd_res.result) except RuntimeError as ex: logger.error(ex.args[0]) return ex.args[1] if len(ex.args) >= 2 else -1 diff --git a/src/azure/cli/tests/test_argparse.py b/src/azure/cli/tests/test_argparse.py index 1ba3540800a..d8b7efd702f 100644 --- a/src/azure/cli/tests/test_argparse.py +++ b/src/azure/cli/tests/test_argparse.py @@ -1,9 +1,17 @@ +try: + # on Python2, we like to use the module "StringIO" rather "io" so to + # avoid "print" errors like: TypeError: string argument expected, got 'str' + from StringIO import StringIO +except ImportError: + # Python 3 + from io import StringIO + import unittest from azure.cli._argparse import ArgumentParser, IncorrectUsageError from azure.cli._logging import logger import logging - +import azure.cli._util as util class Test_argparse(unittest.TestCase): @classmethod @@ -38,31 +46,38 @@ def test_args(self): p = ArgumentParser('test') p.add_command(lambda a, b: (a, b), 'n1', args=[('--arg -a', '', False), ('-b ', '', False)]) - res, other = p.execute('n1 -a x'.split()) + cmd_res = p.execute('n1 -a x'.split()) + res, other = cmd_res.result self.assertTrue(res.arg) self.assertSequenceEqual(res.positional, ['x']) # Should recognize args with alternate prefix - res, other = p.execute('n1 /a'.split()) + cmd_res = p.execute('n1 /a'.split()) + res, other = cmd_res.result self.assertTrue(res.arg) - res, other = p.execute('n1 /arg'.split()) + cmd_res = p.execute('n1 /arg'.split()) + res, other = cmd_res.result self.assertTrue(res.arg) # Should not recognize "------a" - res, other = p.execute('n1 ------a'.split()) + cmd_res = p.execute('n1 ------a'.split()) + res, other = cmd_res.result self.assertNotIn('arg', res) # First two '--' match, so '----a' is added to dict self.assertIn('----a', other) - res = p.execute('n1 -a:x'.split()) + cmd_res = p.execute('n1 -a:x'.split()) + res = cmd_res.result self.assertIsNone(res) - res, other = p.execute('n1 -b -a x'.split()) + cmd_res = p.execute('n1 -b -a x'.split()) + res, other = cmd_res.result self.assertEqual(res.b, '-a') self.assertSequenceEqual(res.positional, ['x']) self.assertRaises(IncorrectUsageError, lambda: res.arg) - res, other = p.execute('n1 -b:-a x'.split()) + cmd_res = p.execute('n1 -b:-a x'.split()) + res, other = cmd_res.result self.assertEqual(res.b, '-a') self.assertSequenceEqual(res.positional, ['x']) self.assertRaises(IncorrectUsageError, lambda: res.arg) @@ -71,15 +86,18 @@ def test_unexpected_args(self): p = ArgumentParser('test') p.add_command(lambda a, b: (a, b), 'n1', args=[('-a', '', False)]) - res, other = p.execute('n1 -b=2'.split()) + cmd_res = p.execute('n1 -b=2'.split()) + res, other = cmd_res.result self.assertFalse(res) self.assertEqual('2', other.b) - res, other = p.execute('n1 -b.c.d=2'.split()) + cmd_res = p.execute('n1 -b.c.d=2'.split()) + res, other = cmd_res.result self.assertFalse(res) self.assertEqual('2', other.b.c.d) - res, other = p.execute('n1 -b.c.d 2 -b.c.e:3'.split()) + cmd_res = p.execute('n1 -b.c.d 2 -b.c.e:3'.split()) + res, other = cmd_res.result self.assertFalse(res) self.assertEqual('2', other.b.c.d) self.assertEqual('3', other.b.c.e) @@ -88,12 +106,63 @@ def test_required_args(self): p = ArgumentParser('test') p.add_command(lambda a, b: (a, b), 'n1', args=[('--arg -a', '', True), ('-b ', '', False)]) - res, other = p.execute('n1 -a x'.split()) + cmd_res = p.execute('n1 -a x'.split()) + res, other = cmd_res.result self.assertTrue(res.arg) self.assertSequenceEqual(res.positional, ['x']) - self.assertIsNone(p.execute('n1 -b x'.split())) + self.assertIsNone(p.execute('n1 -b x'.split()).result) + + def test_args_completion(self): + p = ArgumentParser('test') + p.add_command(lambda a, b: (a, b), 'n1', args=[('--arg -a', '', True), ('-b ', '', False)]) + io = StringIO() + + p.execute('n1 - --complete'.split(), + show_usage=False, + show_completions=True, + out=io) + candidates = util.normalize_newlines(io.getvalue()) + self.assertEqual(candidates, '--arg\n-a\n-b\n') + + io = StringIO() + p.execute('n1 --a --complete'.split(), + show_usage=False, + out=io) + candidates = util.normalize_newlines(io.getvalue()) + self.assertEqual(candidates, '--arg\n') + + io = StringIO() + p.execute('n --a --complete'.split(), + show_usage=False, + show_completions=True, + out=io) + candidates = util.normalize_newlines(io.getvalue()) + self.assertEqual(candidates, 'n1\n') + + def test_specify_output_format(self): + p = ArgumentParser('test') + p.add_command(lambda a, b: (a, b), 'n1', args=[('--arg -a', '', True), ('-b ', '', False)]) + + cmd_res = p.execute('n1 -a x'.split()) + self.assertEqual(cmd_res.output_format, None) + + cmd_res = p.execute('n1 -a x --output json'.split()) + self.assertEqual(cmd_res.output_format, 'json') + + cmd_res = p.execute('n1 -a x --output table'.split()) + self.assertEqual(cmd_res.output_format, 'table') + + cmd_res = p.execute('n1 -a x --output text'.split()) + self.assertEqual(cmd_res.output_format, 'text') + + # Invalid format + cmd_res = p.execute('n1 -a x --output unknown'.split()) + self.assertEqual(cmd_res.output_format, None) + # Invalid format + cmd_res = p.execute('n1 -a x --output'.split()) + self.assertEqual(cmd_res.output_format, None) if __name__ == '__main__': unittest.main() diff --git a/src/azure/cli/tests/test_autocommand.py b/src/azure/cli/tests/test_autocommand.py index 7cc8fddabe2..d352455adff 100644 --- a/src/azure/cli/tests/test_autocommand.py +++ b/src/azure/cli/tests/test_autocommand.py @@ -72,8 +72,8 @@ def testfunc(args, _): p = ArgumentParser('automcommandtest') add_to_parser(p, 'test') - result = p.execute(command_name.split(' ') + '--tre wombat'.split(' ')) - self.assertEqual(result, func) + cmd_res = p.execute(command_name.split(' ') + '--tre wombat'.split(' ')) + self.assertEqual(cmd_res.result, func) if __name__ == '__main__': unittest.main() diff --git a/src/azure/cli/tests/test_output.py b/src/azure/cli/tests/test_output.py index 14ab85cb96c..c2c62f32357 100644 --- a/src/azure/cli/tests/test_output.py +++ b/src/azure/cli/tests/test_output.py @@ -9,7 +9,8 @@ # Python 2 from StringIO import StringIO -from azure.cli._output import OutputProducer, OutputFormatException, format_json, format_table, format_text +from azure.cli._output import (OutputProducer, OutputFormatException, format_json, format_table, format_list, format_text, + ListOutput) import azure.cli._util as util class TestOutput(unittest.TestCase): @@ -42,8 +43,6 @@ def test_out_json_valid(self): """)) def test_out_table_valid(self): - """ - """ output_producer = OutputProducer(formatter=format_table, file=self.io) output_producer.out({'active': True, 'id': '0b1f6472'}) self.assertEqual(util.normalize_newlines(self.io.getvalue()), util.normalize_newlines( @@ -53,5 +52,109 @@ def test_out_table_valid(self): """)) + def test_out_list_valid(self): + output_producer = OutputProducer(formatter=format_list, file=self.io) + output_producer.out({'active': True, 'id': '0b1f6472'}) + self.assertEqual(util.normalize_newlines(self.io.getvalue()), util.normalize_newlines( +"""Active : True +Id : 0b1f6472 + + +""")) + + def test_out_list_valid_none_val(self): + output_producer = OutputProducer(formatter=format_list, file=self.io) + output_producer.out({'active': None, 'id': '0b1f6472'}) + self.assertEqual(util.normalize_newlines(self.io.getvalue()), util.normalize_newlines( +"""Active : None +Id : 0b1f6472 + + +""")) + + 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': []}) + self.assertEqual(util.normalize_newlines(self.io.getvalue()), util.normalize_newlines( +"""Active : None +Id : 0b1f6472 + + HOSTS + None + + +""")) + + def test_out_list_valid_array_complex(self): + output_producer = OutputProducer(formatter=format_list, file=self.io) + output_producer.out([{'active': True, 'id': '783yesdf'}, {'active': False, 'id': '3hjnme32'}, {'active': False, 'id': '23hiujbs'}]) + self.assertEqual(util.normalize_newlines(self.io.getvalue()), util.normalize_newlines( +"""Active : True +Id : 783yesdf + +Active : False +Id : 3hjnme32 + +Active : False +Id : 23hiujbs + + +""")) + + def test_out_list_valid_str_array(self): + output_producer = OutputProducer(formatter=format_list, file=self.io) + output_producer.out(['location', 'id', 'host', 'server']) + self.assertEqual(util.normalize_newlines(self.io.getvalue()), util.normalize_newlines( +"""location + +id + +host + +server + + +""")) + + 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']}) + self.assertEqual(util.normalize_newlines(self.io.getvalue()), util.normalize_newlines( +"""Active : True +Id : 0b1f6472 + + MYARRAY + 1 + 2 + 3 + 4 + + +""")) + + def test_out_list_format_key_simple(self): + lo = ListOutput() + self.assertEqual(lo._formatted_keys_cache, {}) + lo._get_formatted_key('locationId') + self.assertEqual(lo._formatted_keys_cache, {'locationId': 'Location Id'}) + + def test_out_list_format_key_single(self): + lo = ListOutput() + self.assertEqual(lo._formatted_keys_cache, {}) + lo._get_formatted_key('location') + self.assertEqual(lo._formatted_keys_cache, {'location': 'Location'}) + + def test_out_list_format_key_multiple_caps(self): + lo = ListOutput() + self.assertEqual(lo._formatted_keys_cache, {}) + lo._get_formatted_key('fooIDS') + self.assertEqual(lo._formatted_keys_cache, {'fooIDS': 'Foo I D S'}) + + def test_out_list_format_key_multiple_words(self): + lo = ListOutput() + self.assertEqual(lo._formatted_keys_cache, {}) + lo._get_formatted_key('locationIdState') + self.assertEqual(lo._formatted_keys_cache, {'locationIdState': 'Location Id State'}) + if __name__ == '__main__': unittest.main()