diff --git a/src/command_modules/azure-cli-acs/azure/cli/command_modules/acs/_params.py b/src/command_modules/azure-cli-acs/azure/cli/command_modules/acs/_params.py index 2ed0b25f895..02eb3362019 100644 --- a/src/command_modules/azure-cli-acs/azure/cli/command_modules/acs/_params.py +++ b/src/command_modules/azure-cli-acs/azure/cli/command_modules/acs/_params.py @@ -3,13 +3,34 @@ # Licensed under the MIT License. See License.txt in the project root for license information. #--------------------------------------------------------------------------------------------- +# pylint: disable=line-too-long import os import platform -from azure.cli.core.commands import register_cli_argument +from argcomplete.completers import FilesCompleter + +from azure.cli.core.commands import register_cli_argument, CliArgumentType, register_extra_cli_argument from azure.cli.core.commands.parameters import ( name_type, - resource_group_name_type) + resource_group_name_type, + get_one_of_subscription_locations, + get_resource_name_completion_list) + +def _compute_client_factory(**_): + from azure.mgmt.compute import ComputeManagementClient + from azure.cli.core.commands.client_factory import get_mgmt_service_client + return get_mgmt_service_client(ComputeManagementClient) + +def get_vm_sizes(location): + return list(_compute_client_factory().virtual_machine_sizes.list(location)) + +def get_vm_size_completion_list(prefix, action, parsed_args, **kwargs):#pylint: disable=unused-argument + try: + location = parsed_args.location + except AttributeError: + location = get_one_of_subscription_locations() + result = get_vm_sizes(location) + return [r.name for r in result] def _get_default_install_location(exe_name): system = platform.system() @@ -24,6 +45,23 @@ def _get_default_install_location(exe_name): install_location = None return install_location + +name_arg_type = CliArgumentType(options_list=('--name', '-n'), metavar='NAME') + +register_cli_argument('acs', 'name', arg_type=name_arg_type) +register_cli_argument('acs', 'orchestrator_type', type=str) +#some admin names are prohibited in acs, such as root, admin, etc. Because we have no control on the orchestrators, so default to a safe name. +register_cli_argument('acs', 'admin_username', options_list=('--admin-username',), default='azureuser', required=False) +register_cli_argument('acs', 'dns_name_prefix', options_list=('--dns-prefix', '-d')) +register_cli_argument('acs', 'container_service_name', options_list=('--name', '-n'), help='The name of the container service', completer=get_resource_name_completion_list('Microsoft.ContainerService/ContainerServices')) + +register_cli_argument('acs', 'ssh_key_value', required=False, help='SSH key file value or key file path.', default=os.path.join(os.path.expanduser('~'), '.ssh', 'id_rsa.pub'), completer=FilesCompleter()) + +register_extra_cli_argument('acs create', 'generate_ssh_keys', action='store_true', help='Generate SSH public and private key files if missing') +register_cli_argument('acs create', 'agent_vm_size', completer=get_vm_size_completion_list) +register_cli_argument('acs create', 'service_principal', help='Service principal for making calls into Azure APIs') +register_cli_argument('acs create', 'client_secret', help='Client secret to use with the service principal for making calls to Azure APIs') + register_cli_argument('acs dcos browse', 'name', name_type) register_cli_argument('acs dcos browse', 'resource_group_name', resource_group_name_type) register_cli_argument('acs dcos install-cli', 'install_location', @@ -32,3 +70,12 @@ def _get_default_install_location(exe_name): register_cli_argument('acs dcos install-cli', 'client_version', options_list=('--client-version',), default='1.8') +register_cli_argument('acs kubernetes install-cli', 'install_location', + options_list=('--install-location',), + default=_get_default_install_location('kubectl')) +register_cli_argument('acs kubernetes install-cli', 'client_version', + options_list=('--client-version',), + default='1.4.5') +# TODO: Make this derive from the cluster object, instead of just preset values +register_cli_argument('acs kubernetes get-credentials', 'dns_prefix') +register_cli_argument('acs kubernetes get-credentials', 'location') diff --git a/src/command_modules/azure-cli-acs/azure/cli/command_modules/acs/acs_client.py b/src/command_modules/azure-cli-acs/azure/cli/command_modules/acs/acs_client.py index 77c4786d099..ae8a512eaef 100644 --- a/src/command_modules/azure-cli-acs/azure/cli/command_modules/acs/acs_client.py +++ b/src/command_modules/azure-cli-acs/azure/cli/command_modules/acs/acs_client.py @@ -3,6 +3,7 @@ # Licensed under the MIT License. See License.txt in the project root for license information. #--------------------------------------------------------------------------------------------- +import os.path import socket import threading import webbrowser @@ -10,7 +11,19 @@ import paramiko from sshtunnel import SSHTunnelForwarder +from scp import SCPClient +def SecureCopy(user, host, src, dest): + home = os.path.expanduser("~") + ssh = paramiko.SSHClient() + ssh.load_system_host_keys() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh.connect(host, username=user, key_filename=os.path.join(home, '.ssh', 'id_rsa')) + + scp = SCPClient(ssh.get_transport()) + + scp.get(src, dest) + scp.close() class ACSClient(object): def __init__(self, client=None): diff --git a/src/command_modules/azure-cli-acs/azure/cli/command_modules/acs/commands.py b/src/command_modules/azure-cli-acs/azure/cli/command_modules/acs/commands.py index 9c94ac9250e..3fa364631a4 100644 --- a/src/command_modules/azure-cli-acs/azure/cli/command_modules/acs/commands.py +++ b/src/command_modules/azure-cli-acs/azure/cli/command_modules/acs/commands.py @@ -9,4 +9,7 @@ cli_command(__name__, 'acs dcos browse', 'azure.cli.command_modules.acs.custom#dcos_browse') cli_command(__name__, 'acs dcos install-cli', 'azure.cli.command_modules.acs.custom#dcos_install_cli') +cli_command(__name__, 'acs create', 'azure.cli.command_modules.acs.custom#acs_create') +cli_command(__name__, 'acs kubernetes install-cli', 'azure.cli.command_modules.acs.custom#k8s_install_cli') +cli_command(__name__, 'acs kubernetes get-credentials', 'azure.cli.command_modules.acs.custom#acs_get_credentials') diff --git a/src/command_modules/azure-cli-acs/azure/cli/command_modules/acs/custom.py b/src/command_modules/azure-cli-acs/azure/cli/command_modules/acs/custom.py index c3384a9f81d..abbd92fbe69 100644 --- a/src/command_modules/azure-cli-acs/azure/cli/command_modules/acs/custom.py +++ b/src/command_modules/azure-cli-acs/azure/cli/command_modules/acs/custom.py @@ -3,17 +3,29 @@ # Licensed under the MIT License. See License.txt in the project root for license information. #--------------------------------------------------------------------------------------------- +from __future__ import print_function +import binascii +import json +import os +import os.path import platform import random import string +import sys from six.moves.urllib.request import urlretrieve #pylint: disable=import-error +import time + +from msrestazure.azure_exceptions import CloudError import azure.cli.core._logging as _logging from azure.cli.command_modules.acs import acs_client, proxy +from azure.cli.command_modules.vm.mgmt_acs.lib import \ + AcsCreationClient as ACSClient # pylint: disable=too-few-public-methods,too-many-arguments,no-self-use,line-too-long from azure.cli.core._util import CLIError from azure.cli.core.commands.client_factory import get_mgmt_service_client from azure.mgmt.compute import ComputeManagementClient +from azure.mgmt.resource.resources import ResourceManagementClient logger = _logging.get_az_logger(__name__) @@ -84,6 +96,227 @@ def dcos_install_cli(install_location=None, client_version='1.8'): except IOError as err: raise CLIError('Connection error while attempting to download client ({})'.format(err)) +def k8s_install_cli(client_version="1.4.5", install_location=None): + """ + Downloads the kubectl command line from Kubernetes + """ + file_url = '' + system = platform.system() + if system == 'Windows': + file_url = 'https://storage.googleapis.com/kubernetes-release/release/v{}/bin/windows/amd64/kubectl.exe'.format(client_version) + elif system == 'Linux': + file_url = 'https://storage.googleapis.com/kubernetes-release/release/v{}/bin/linux/amd64/kubectl'.format(client_version) + elif system == 'Darwin': + file_url = 'https://storage.googleapis.com/kubernetes-release/release/v{}/darwin/amd64/kubectl'.format(client_version) + else: + raise CLIError('Proxy server ({}) does not exist on the cluster.'.format(system)) + + logger.info('Downloading client to %s', install_location) + try: + urlretrieve(file_url, install_location) + except IOError as err: + raise CLIError('Connection error while attempting to download client ({})'.format(err)) + +def _build_service_principal(name, url, client_secret): + from azure.cli.command_modules.role.custom import ( + _graph_client_factory, + create_application, + create_service_principal, + ) + + sys.stdout.write('creating service principal') + result = create_application(_graph_client_factory().applications, name, url, [url], password=client_secret) + service_principal = result.app_id #pylint: disable=no-member + for x in range(0, 10): + try: + create_service_principal(service_principal) + # TODO figure out what exception AAD throws here sometimes. + except: #pylint: disable=bare-except + sys.stdout.write('.') + sys.stdout.flush() + time.sleep(2 + 2 * x) + print('done') + return service_principal + +def _add_role_assignment(role, service_principal): + # AAD can have delays in propogating data, so sleep and retry + sys.stdout.write('waiting for AAD role to propogate.') + for x in range(0, 10): + from azure.cli.command_modules.role.custom import create_role_assignment + try: + # TODO: break this out into a shared utility library + create_role_assignment(role, service_principal) + break + except CloudError as ex: + if ex.message == 'The role assignment already exists.': + break + sys.stdout.write('.') + logger.info('%s', ex.message) + time.sleep(2 + 2 * x) + except: #pylint: disable=bare-except + sys.stdout.write('.') + time.sleep(2 + 2 * x) + print('done') + +def acs_create(resource_group_name, deployment_name, dns_name_prefix, name, ssh_key_value, content_version=None, admin_username="azureuser", agent_count="3", agent_vm_size="Standard_D2_v2", location=None, master_count="3", orchestrator_type="dcos", service_principal=None, client_secret=None, tags=None, custom_headers=None, raw=False, **operation_config): + """Create a new Acs. + :param resource_group_name: The name of the resource group. The name + is case insensitive. + :type resource_group_name: str + :param deployment_name: The name of the deployment. + :type deployment_name: str + :param dns_name_prefix: Sets the Domain name prefix for the cluster. + The concatenation of the domain name and the regionalized DNS zone + make up the fully qualified domain name associated with the public + IP address. + :type dns_name_prefix: str + :param name: Resource name for the container service. + :type name: str + :param ssh_key_value: Configure all linux machines with the SSH RSA + public key string. Your key should include three parts, for example + 'ssh-rsa AAAAB...snip...UcyupgH azureuser@linuxvm + :type ssh_key_value: str + :param content_version: If included it must match the ContentVersion + in the template. + :type content_version: str + :param admin_username: User name for the Linux Virtual Machines. + :type admin_username: str + :param agent_count: The number of agents for the cluster. Note, for + DC/OS clusters you will also get 1 or 2 public agents in addition to + these seleted masters. + :type agent_count: str + :param agent_vm_size: The size of the Virtual Machine. + :type agent_vm_size: str + :param location: Location for VM resources. + :type location: str + :param master_count: The number of DC/OS masters for the cluster. + :type master_count: str + :param orchestrator_type: The type of orchestrator used to manage the + applications on the cluster. Possible values include: 'dcos', 'swarm' + :type orchestrator_type: str or :class:`orchestratorType + ` + :param tags: Tags object. + :type tags: object + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :rtype: + :class:`AzureOperationPoller` + instance that returns :class:`DeploymentExtended + ` + :rtype: :class:`ClientRawResponse` + if raw=true + :raises: :class:`CloudError` + """ + if orchestrator_type == 'Kubernetes' or orchestrator_type == 'kubernetes': + principalObj = load_acs_service_principal() + if principalObj: + service_principal = principalObj.get('service_principal') + client_secret = principalObj.get('client_secret') + + if not service_principal: + if not client_secret: + client_secret = binascii.b2a_hex(os.urandom(10)).decode('utf-8') + store_acs_service_principal(client_secret, None) + salt = binascii.b2a_hex(os.urandom(3)).decode('utf-8') + url = 'http://{}.{}-k8s-masters.{}.cloudapp.azure.com'.format(salt, dns_name_prefix, location) + + service_principal = _build_service_principal(name, url, client_secret) + logger.info('Created a service principal: %s', service_principal) + store_acs_service_principal(client_secret, service_principal) + _add_role_assignment('Owner', service_principal) + return _create_kubernetes(resource_group_name, deployment_name, dns_name_prefix, name, ssh_key_value, admin_username=admin_username, agent_count=agent_count, agent_vm_size=agent_vm_size, location=location, service_principal=service_principal, client_secret=client_secret) + + ops = get_mgmt_service_client(ACSClient).acs + return ops.create_or_update(resource_group_name, deployment_name, dns_name_prefix, name, ssh_key_value, content_version=content_version, admin_username=admin_username, agent_count=agent_count, agent_vm_size=agent_vm_size, location=location, master_count=master_count, orchestrator_type=orchestrator_type, tags=tags, custom_headers=custom_headers, raw=raw, operation_config=operation_config) + +def store_acs_service_principal(client_secret, service_principal): + obj = {} + if client_secret: + obj['client_secret'] = client_secret + if service_principal: + obj['service_principal'] = service_principal + configPath = os.path.join(os.path.expanduser('~'), '.azure', 'acsServicePrincipal.json') + with os.fdopen(os.open(configPath, os.O_RDWR|os.O_CREAT|os.O_TRUNC, 0o600), + 'w+') as spFile: + json.dump(obj, spFile) + +def load_acs_service_principal(): + configPath = os.path.join(os.path.expanduser('~'), '.azure', 'acsServicePrincipal.json') + if not os.path.exists(configPath): + return None + fd = os.open(configPath, os.O_RDONLY) + try: + return json.loads(os.fdopen(fd).read()) + except: #pylint: disable=bare-except + return None + +def _create_kubernetes(resource_group_name, deployment_name, dns_name_prefix, name, ssh_key_value, admin_username="azureuser", agent_count="3", agent_vm_size="Standard_D2_v2", location=None, service_principal=None, client_secret=None): + from azure.mgmt.resource.resources.models import DeploymentProperties + if not location: + location = '[resourceGroup().location]' + template = { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [ + { + "apiVersion": "2016-09-30", + "location": location, + "type": "Microsoft.ContainerService/containerServices", + "name": name, + "properties": { + "orchestratorProfile": { + "orchestratorType": "Custom" + }, + "masterProfile": { + "count": 1, + "dnsPrefix": dns_name_prefix + '-k8s-masters' + }, + "agentPoolProfiles": [ + { + "name": "agentpools", + "count": agent_count, + "vmSize": agent_vm_size, + "dnsPrefix": dns_name_prefix + '-k8s-agents', + } + ], + "linuxProfile": { + "ssh": { + "publicKeys": [ + { + "keyData": ssh_key_value + } + ] + }, + "adminUsername": admin_username + }, + "servicePrincipalProfile": { + "ClientId": service_principal, + "Secret": client_secret + }, + "customProfile": { + "orchestrator": "kubernetes" + } + } + } + ] + } + + properties = DeploymentProperties(template=template, template_link=None, + parameters=None, mode='incremental') + smc = get_mgmt_service_client(ResourceManagementClient) + return smc.deployments.create_or_update(resource_group_name, deployment_name, properties) + +def acs_get_credentials(dns_prefix, location): + # TODO: once we get the right swagger in here, update this to actually pull location and dns_prefix + #acs_info = _get_acs_info(name, resource_group_name) + home = os.path.expanduser('~') + + path = os.path.join(home, '.kube', 'config') + # TODO: this only works for public cloud, need other casing for national clouds + acs_client.SecureCopy('azureuser', '{}-k8s-masters.{}.cloudapp.azure.com'.format(dns_prefix, location), + '.kube/config', path) + def _get_host_name(acs_info): """ Gets the FQDN from the acs_info object. diff --git a/src/command_modules/azure-cli-acs/setup.py b/src/command_modules/azure-cli-acs/setup.py index 8eca0fc2576..f3f0f65a32b 100644 --- a/src/command_modules/azure-cli-acs/setup.py +++ b/src/command_modules/azure-cli-acs/setup.py @@ -28,6 +28,7 @@ 'paramiko', 'pyyaml', 'six', + 'scp', 'sshtunnel' ] diff --git a/src/command_modules/azure-cli-vm/azure/cli/command_modules/vm/_params.py b/src/command_modules/azure-cli-vm/azure/cli/command_modules/vm/_params.py index 91020eac96a..05c2cadf897 100644 --- a/src/command_modules/azure-cli-vm/azure/cli/command_modules/vm/_params.py +++ b/src/command_modules/azure-cli-vm/azure/cli/command_modules/vm/_params.py @@ -6,13 +6,11 @@ # pylint: disable=line-too-long import argparse import getpass -import os from argcomplete.completers import FilesCompleter from azure.mgmt.compute.models import (VirtualHardDisk, CachingTypes, - ContainerServiceOchestratorTypes, UpgradeMode) from azure.mgmt.storage.models import SkuName from azure.cli.core.commands import register_cli_argument, CliArgumentType, register_extra_cli_argument @@ -70,15 +68,16 @@ def get_vm_size_completion_list(prefix, action, parsed_args, **kwargs):#pylint: register_cli_argument('vm access', 'password', options_list=('--password', '-p'), help='The user password') register_cli_argument('acs', 'name', arg_type=name_arg_type) -register_cli_argument('acs', 'orchestrator_type', **enum_choice_list(ContainerServiceOchestratorTypes)) +register_cli_argument('acs', 'orchestrator_type', type=str) #some admin names are prohibited in acs, such as root, admin, etc. Because we have no control on the orchestrators, so default to a safe name. register_cli_argument('acs', 'admin_username', options_list=('--admin-username',), default='azureuser', required=False) -register_cli_argument('acs', 'ssh_key_value', required=False, help='SSH key file value or key file path.', default=os.path.join(os.path.expanduser('~'), '.ssh', 'id_rsa.pub'), completer=FilesCompleter()) register_cli_argument('acs', 'dns_name_prefix', options_list=('--dns-prefix', '-d')) register_extra_cli_argument('acs create', 'generate_ssh_keys', action='store_true', help='Generate SSH public and private key files if missing') register_cli_argument('acs', 'container_service_name', options_list=('--name', '-n'), help='The name of the container service', completer=get_resource_name_completion_list('Microsoft.ContainerService/ContainerServices')) register_cli_argument('acs create', 'agent_vm_size', completer=get_vm_size_completion_list) register_cli_argument('acs scale', 'new_agent_count', type=int, help='The number of agents for the cluster') +register_cli_argument('acs create', 'service_principal', help='Service principal for making calls into Azure APIs') +register_cli_argument('acs create', 'client_secret', help='Client secret to use with the service principal for making calls to Azure APIs') register_cli_argument('vm capture', 'overwrite', action='store_true') diff --git a/src/command_modules/azure-cli-vm/azure/cli/command_modules/vm/commands.py b/src/command_modules/azure-cli-vm/azure/cli/command_modules/vm/commands.py index 5e3a2b3a20f..1771f6041ab 100644 --- a/src/command_modules/azure-cli-vm/azure/cli/command_modules/vm/commands.py +++ b/src/command_modules/azure-cli-vm/azure/cli/command_modules/vm/commands.py @@ -7,7 +7,7 @@ from azure.cli.core.commands.arm import cli_generic_update_command -from azure.cli.command_modules.vm._client_factory import * #pylint: disable=wildcard-import +from azure.cli.command_modules.vm._client_factory import * #pylint: disable=wildcard-import,unused-wildcard-import #pylint: disable=line-too-long @@ -72,8 +72,6 @@ cli_command(__name__, 'vm boot-diagnostics get-boot-log', 'azure.cli.command_modules.vm.custom#get_boot_log') # ACS -cli_command(__name__, 'acs create', 'azure.cli.command_modules.vm.mgmt_acs.lib.operations.acs_operations#AcsOperations.create_or_update', cf_acs_create, - transform=DeploymentOutputLongRunningOperation('Starting container service create')) #Remove the hack after https://github.com/Azure/azure-rest-api-specs/issues/352 fixed from azure.mgmt.compute.models import ContainerService#pylint: disable=wrong-import-position diff --git a/src/command_modules/azure-cli-vm/azure/cli/command_modules/vm/tests/test_vm_commands.py b/src/command_modules/azure-cli-vm/azure/cli/command_modules/vm/tests/test_vm_commands.py index 67ff89733e6..712dd881a31 100644 --- a/src/command_modules/azure-cli-vm/azure/cli/command_modules/vm/tests/test_vm_commands.py +++ b/src/command_modules/azure-cli-vm/azure/cli/command_modules/vm/tests/test_vm_commands.py @@ -1256,8 +1256,8 @@ def body(self): #create self.cmd('acs create -g {} -n {} --dns-prefix {}'.format(self.resource_group, acs_name, dns_prefix), checks=[ - JMESPathCheck('masterFQDN', '{}mgmt.{}.cloudapp.azure.com'.format(dns_prefix, self.location)), - JMESPathCheck('agentFQDN', '{}agents.{}.cloudapp.azure.com'.format(dns_prefix, self.location)) + JMESPathCheck('properties.outputs.masterFQDN.value', '{}mgmt.{}.cloudapp.azure.com'.format(dns_prefix, self.location)), + JMESPathCheck('properties.outputs.agentFQDN.value', '{}agents.{}.cloudapp.azure.com'.format(dns_prefix, self.location)) ]) #show self.cmd('acs show -g {} -n {}'.format(self.resource_group, acs_name), checks=[