diff --git a/cleanup.sh b/cleanup.sh new file mode 100755 index 000000000..627a3b3ed --- /dev/null +++ b/cleanup.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +rm -rf build dist *.egg-info +find . -name '*.pyc' -delete +find . -name '*.log' -delete diff --git a/container_service_extension/broker.py b/container_service_extension/broker.py index 8f1645906..58899f06a 100644 --- a/container_service_extension/broker.py +++ b/container_service_extension/broker.py @@ -1,9 +1,19 @@ # SPDX-License-Identifier: BSD-2-Clause import click + +from container_service_extension.cluster import add_nodes +from container_service_extension.cluster import get_master_ip +from container_service_extension.cluster import init_cluster +from container_service_extension.cluster import join_cluster from container_service_extension.cluster import load_from_metadata +from container_service_extension.cluster import TYPE_MASTER +from container_service_extension.cluster import TYPE_NODE + import logging + import pkg_resources + from pyvcloud.vcd.client import _WellKnownEndpoint from pyvcloud.vcd.client import BasicLoginCredentials from pyvcloud.vcd.client import Client @@ -12,10 +22,10 @@ from pyvcloud.vcd.task import Task from pyvcloud.vcd.vapp import VApp from pyvcloud.vcd.vdc import VDC + import re import requests import threading -import time import traceback import uuid import yaml @@ -39,6 +49,8 @@ SAMPLE_TEMPLATE_PHOTON_V1 = { 'name': + 'photon-v1', + 'catalog_item': 'photon-custom-hw11-1.0-62c543d-k8s', 'source_ova_name': 'photon-custom-hw11-1.0-62c543d.ova', @@ -55,11 +67,15 @@ 'mem': 2048, 'admin_password': - 'guest_os_admin_password' + 'guest_os_admin_password', + 'description': + "PhotonOS v1\nDocker 17.06.0-1\nKubernetes 1.8.1\nweave 2.0.5" } SAMPLE_TEMPLATE_UBUNTU_16_04 = { 'name': + 'ubuntu-16.04', + 'catalog_item': 'ubuntu-16.04-server-cloudimg-amd64-k8s', 'source_ova_name': 'ubuntu-16.04-server-cloudimg-amd64.ova', @@ -76,7 +92,9 @@ 'mem': 2048, 'admin_password': - 'guest_os_admin_password' + 'guest_os_admin_password', + 'description': + 'Ubuntu 16.04\nDocker 17.09.0~ce\nKubernetes 1.8.2\nweave 2.0.5' } SAMPLE_CONFIG = { @@ -115,7 +133,7 @@ def validate_broker_config_elements(config): raise Exception('invalid key: %s' % k) -def validate_broker_config_content(config, client): +def validate_broker_config_content(config, client, template): from container_service_extension.config import bool_to_msg logged_in_org = client.get_org() org = Org(client, resource=logged_in_org) @@ -123,17 +141,19 @@ def validate_broker_config_content(config, client): click.echo('Find catalog \'%s\': %s' % (config['broker']['catalog'], bool_to_msg(True))) default_template_found = False - for template in config['broker']['templates']: - click.secho('Validating template: %s' % template['name']) - if config['broker']['default_template'] == template['name']: - default_template_found = True - click.secho(' Is default template: %s' % True) - else: - click.secho(' Is default template: %s' % False) - org.get_catalog_item(config['broker']['catalog'], template['name']) - click.echo('Find template \'%s\', \'%s\': %s' % - (config['broker']['catalog'], template['name'], - bool_to_msg(True))) + for t in config['broker']['templates']: + if template == '*' or template == t['name']: + click.secho('Validating template: %s' % t['name']) + if config['broker']['default_template'] == t['name']: + default_template_found = True + click.secho(' Is default template: %s' % True) + else: + click.secho(' Is default template: %s' % False) + org.get_catalog_item(config['broker']['catalog'], + t['catalog_item']) + click.echo('Find template \'%s\', \'%s\': %s' % + (config['broker']['catalog'], t['catalog_item'], + bool_to_msg(True))) assert default_template_found @@ -145,22 +165,6 @@ def get_new_broker(config): return None -def wait_until_tools_ready(vm): - while True: - try: - status = vm.guest.toolsRunningStatus - if 'guestToolsRunning' == status: - LOGGER.debug('vm tools %s are ready' % vm) - return - LOGGER.debug('waiting for vm tools %s to be ready (%s)' % (vm, - status)) - time.sleep(1) - except Exception: - LOGGER.debug('waiting for vm tools %s to be ready (%s)* ' % - (vm, status)) - time.sleep(1) - - def spinning_cursor(): while True: for cursor in '|/-\\': @@ -247,8 +251,8 @@ def update_task(self, status, operation, message=None, error_message=None): self.t = self.task.update( status.value, 'vcloud.cse', - operation, message, + operation, '', None, 'urn:cse:cluster:%s' % self.cluster_id, @@ -271,6 +275,16 @@ def is_valid_name(self, name): allowed = re.compile("(?!-)[A-Z\d-]{1,63}(? 0: + + self.update_task( + TaskStatus.RUNNING, + self.op, + message='Creating %s node(s) for %s(%s)' % + (self.body['node_count'], self.cluster_name, + self.cluster_id)) + add_nodes( + self.body['node_count'], + template, + TYPE_NODE, + self.config, + self.client_tenant, + org, + vdc, + vapp, + self.body, + wait=True) + self.update_task( + TaskStatus.RUNNING, + self.op, + message='Adding %s node(s) to %s(%s)' % + (self.body['node_count'], self.cluster_name, + self.cluster_id)) + vapp.reload() + join_cluster(self.config, vapp, template) self.update_task( TaskStatus.SUCCESS, diff --git a/container_service_extension/client/__init__.py b/container_service_extension/client/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/container_service_extension/client/cluster.py b/container_service_extension/client/cluster.py new file mode 100644 index 000000000..cd49071c6 --- /dev/null +++ b/container_service_extension/client/cluster.py @@ -0,0 +1,129 @@ +# container-service-extension +# Copyright (c) 2017 VMware, Inc. All Rights Reserved. +# SPDX-License-Identifier: BSD-2-Clause + + +import json +import requests + + +class Cluster(object): + def __init__(self, client): + self.client = client + self._uri = self.client.get_api_uri() + '/cse' + + def get_templates(self): + method = 'GET' + uri = '%s/template' % (self._uri) + response = self.client._do_request_prim( + method, + uri, + self.client._session, + contents=None, + media_type=None, + accept_type='application/*+json', + auth=None) + if response.status_code == requests.codes.ok: + return json.loads(response.content.decode("utf-8")) + else: + raise Exception(json.loads(response.content)) + + def get_clusters(self): + method = 'GET' + uri = self._uri + response = self.client._do_request_prim( + method, + uri, + self.client._session, + contents=None, + media_type=None, + accept_type='application/*+json', + auth=None) + if response.status_code == requests.codes.ok: + return json.loads(response.content.decode("utf-8")) + else: + raise Exception(json.loads(response.content)) + + def create_cluster(self, + vdc, + network_name, + name, + node_count=2, + cpu_count=None, + memory=None, + storage_profile=None, + ssh_key=None, + template=None): + """Create a new Kubernetes cluster + + :param vdc: (str): The name of the vdc backing the org in which the + cluster would be created + :param network_name: (str): The name of the network to which the + cluster vApp will connect to + :param name: (str): The name of the cluster + :param node_count: (str): The number of slave nodes + :param cpu_count: (str): The number of virtual cpus on each of the + nodes in the cluster + :param memory: (str): The amount of memory (in MB) on each of the nodes + in the cluster + :param storage_profile: (str): The name of the storage profile which + will back the cluster + :param ssh_key: (str): The ssh key that clients can use to log into the + node vms without explicitly providing passwords + :param template: (str): The name of the catalog template to + instantiate the nodes from + :return: (json) A parsed json object describing the requested cluster. + """ + method = 'POST' + uri = self._uri + data = { + 'name': name, + 'node_count': node_count, + 'vdc': vdc, + 'cpu_count': cpu_count, + 'memory': memory, + 'network': network_name, + 'storage_profile': storage_profile, + 'ssh_key': ssh_key, + 'template': template + } + response = self.client._do_request_prim( + method, + uri, + self.client._session, + contents=data, + media_type=None, + accept_type='application/*+json') + if response.status_code == requests.codes.accepted: + return json.loads(response.content) + else: + raise Exception(json.loads(response.content).get('message')) + + def delete_cluster(self, cluster_name): + method = 'DELETE' + uri = '%s/%s' % (self._uri, cluster_name) + response = self.client._do_request_prim( + method, + uri, + self.client._session, + accept_type='application/*+json') + if response.status_code == requests.codes.accepted: + return json.loads(response.content) + else: + raise Exception(json.loads(response.content).get('message')) + + def get_config(self, cluster_name): + method = 'GET' + uri = '%s/%s/config' % (self._uri, cluster_name) + response = self.client._do_request_prim( + method, + uri, + self.client._session, + contents=None, + media_type=None, + accept_type='text/x-yaml', + auth=None) + if response.status_code == requests.codes.ok: + return response.content.decode('utf-8').replace('\\n', '\n')[1:-1] + else: + raise Exception(json.loads(response.content)) diff --git a/container_service_extension/client/cse.py b/container_service_extension/client/cse.py new file mode 100644 index 000000000..fd6bb0ce9 --- /dev/null +++ b/container_service_extension/client/cse.py @@ -0,0 +1,331 @@ +# container-service-extension +# Copyright (c) 2017 VMware, Inc. All Rights Reserved. +# SPDX-License-Identifier: BSD-2-Clause + +from os.path import expanduser +from os.path import join + +import click + +from container_service_extension.client.cluster import Cluster + +from vcd_cli.utils import restore_session +from vcd_cli.utils import stderr +from vcd_cli.utils import stdout +from vcd_cli.vcd import abort_if_false +from vcd_cli.vcd import vcd + +import yaml + +@vcd.group(short_help='manage clusters') +@click.pass_context +def cse(ctx): + """Work with kubernetes clusters in vCloud Director. + +\b + Examples + vcd cse cluster list + Get list of kubernetes clusters in current virtual datacenter. +\b + vcd cse cluster create dev-cluster --network net1 + Create a kubernetes cluster in current virtual datacenter. +\b + vcd cse cluster create prod-cluster --nodes 4 \\ + --network net1 --storage-profile '*' + Create a kubernetes cluster with 4 worker nodes. +\b + vcd cse cluster delete dev-cluster + Delete a kubernetes cluster by name. +\b + vcd cse cluster create c1 --nodes 0 --network net1 + Create a single node kubernetes cluster for dev/test. +\b + vcd cse template list + Get list of CSE templates available. + """ + + if ctx.invoked_subcommand is not None: + try: + restore_session(ctx) + if not ctx.obj['profiles'].get('vdc_in_use') or \ + not ctx.obj['profiles'].get('vdc_href'): + raise Exception('select a virtual datacenter') + except Exception as e: + stderr(e, ctx) + + +@cse.group(short_help='work with clusters') +@click.pass_context +def cluster(ctx): + """Work with kubernetes clusters.""" + + if ctx.invoked_subcommand is not None: + try: + restore_session(ctx) + if not ctx.obj['profiles'].get('vdc_in_use') or \ + not ctx.obj['profiles'].get('vdc_href'): + raise Exception('select a virtual datacenter') + except Exception as e: + stderr(e, ctx) + + +@cse.group(short_help='manage CSE templates') +@click.pass_context +def template(ctx): + """Work with CSE templates.""" + + if ctx.invoked_subcommand is not None: + try: + restore_session(ctx) + if not ctx.obj['profiles'].get('vdc_in_use') or \ + not ctx.obj['profiles'].get('vdc_href'): + raise Exception('select a virtual datacenter') + except Exception as e: + stderr(e, ctx) + + +@template.command('list', short_help='list templates') +@click.pass_context +def list_templates(ctx): + try: + client = ctx.obj['client'] + cluster = Cluster(client) + result = [] + templates = cluster.get_templates() + for t in templates: + result.append({'name': t['name'], + 'description': t['description'], + 'catalog': t['catalog'], + 'catalog_item': t['catalog_item'], + 'is_default': t['is_default'], + }) + stdout(result, ctx, show_id=True) + except Exception as e: + stderr(e, ctx) + + +@cluster.command('list', short_help='list clusters') +@click.pass_context +def list_clusters(ctx): + try: + client = ctx.obj['client'] + cluster = Cluster(client) + result = [] + clusters = cluster.get_clusters() + for c in clusters: + result.append({'name': c['name'], + 'IP master': c['leader_endpoint'], + 'VMs': c['number_of_vms'], + 'vdc': c['vdc_name'] + }) + stdout(result, ctx, show_id=True) + except Exception as e: + stderr(e, ctx) + + +@cluster.command(short_help='delete cluster') +@click.pass_context +@click.argument('name', + metavar='', + required=True) +@click.option('-y', + '--yes', + is_flag=True, + callback=abort_if_false, + expose_value=False, + prompt='Are you sure you want to delete the cluster?') +def delete(ctx, name): + try: + client = ctx.obj['client'] + cluster = Cluster(client) + result = cluster.delete_cluster(name) + stdout(result, ctx) + except Exception as e: + stderr(e, ctx) + + +@cluster.command(short_help='create cluster') +@click.pass_context +@click.argument('name', + metavar='', + required=True) +@click.option('-N', + '--nodes', + 'node_count', + required=False, + default=2, + metavar='', + help='Number of nodes to create') +@click.option('-c', + '--cpu', + 'cpu_count', + required=False, + default=None, + metavar='', + help='Number of virtual cpus on each node') +@click.option('-m', + '--memory', + 'memory', + required=False, + default=None, + metavar='', + help='Amount of memory (in MB) on each node') +@click.option('-n', + '--network', + 'network_name', + default=None, + required=False, + metavar='', + help='Network name') +@click.option('-s', + '--storage-profile', + 'storage_profile', + required=False, + default=None, + metavar='', + help='Name of the storage profile for the nodes') +@click.option('-k', + '--ssh-key', + 'ssh_key_file', + required=False, + default=None, + type=click.File('r'), + metavar='', + help='SSH public key to connect to the guest OS on the VM') +@click.option('-t', + '--template', + 'template', + required=False, + default=None, + metavar='