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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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',
Expand All @@ -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')
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,27 @@
# Licensed under the MIT License. See License.txt in the project root for license information.
#---------------------------------------------------------------------------------------------

import os.path
import socket
import threading
import webbrowser
from time import sleep

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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

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

mgmt_acs should be moved into this command module rather than the acs module having a dependency on vm.

Copy link
Contributor

Choose a reason for hiding this comment

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

@derekbekoe, this will be cleaned up in 2 weeks per conversation with Brendan

Copy link
Member Author

Choose a reason for hiding this comment

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

ack. I promise to fix this asap, but I need to get this in for the 11/11 cut.

Thanks for the flexibility.

# 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__)

Expand Down Expand Up @@ -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
<Default.models.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<msrestazure.azure_operation.AzureOperationPoller>`
instance that returns :class:`DeploymentExtended
<Default.models.DeploymentExtended>`
:rtype: :class:`ClientRawResponse<msrest.pipeline.ClientRawResponse>`
if raw=true
:raises: :class:`CloudError<msrestazure.azure_exceptions.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.
Expand Down
1 change: 1 addition & 0 deletions src/command_modules/azure-cli-acs/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
'paramiko',
'pyyaml',
'six',
'scp',
'sshtunnel'
]

Expand Down
Loading