From eacf878ec5372a6db4596c9cbc78ef96bf2453b9 Mon Sep 17 00:00:00 2001 From: "Yang (Gabriel) Yu" Date: Wed, 15 Aug 2018 12:39:33 +0800 Subject: add k8s capacity test case JIRA: BOTTLENECK-243 Change-Id: I0f36aac10cf0e05560051c785ada397e0c97e112 Signed-off-by: Yang (Gabriel) Yu --- testsuites/kubestone/__init__.py | 0 testsuites/kubestone/samples/__init__.py | 0 .../kubestone/samples/deployment_sample.yaml | 21 +++ testsuites/kubestone/samples/pod_sample.yaml | 11 ++ testsuites/kubestone/stress_test.py | 156 +++++++++++++++++++++ testsuites/kubestone/testcases/__init__.py | 0 .../kubestone/testcases/deployment_capacity.yaml | 25 ++++ utils/k8s_setup/k8s_config_pre.sh | 2 +- utils/k8s_setup/k8s_utils.py | 53 ++++++- 9 files changed, 260 insertions(+), 8 deletions(-) create mode 100644 testsuites/kubestone/__init__.py create mode 100644 testsuites/kubestone/samples/__init__.py create mode 100644 testsuites/kubestone/samples/deployment_sample.yaml create mode 100644 testsuites/kubestone/samples/pod_sample.yaml create mode 100644 testsuites/kubestone/stress_test.py create mode 100644 testsuites/kubestone/testcases/__init__.py create mode 100644 testsuites/kubestone/testcases/deployment_capacity.yaml diff --git a/testsuites/kubestone/__init__.py b/testsuites/kubestone/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/testsuites/kubestone/samples/__init__.py b/testsuites/kubestone/samples/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/testsuites/kubestone/samples/deployment_sample.yaml b/testsuites/kubestone/samples/deployment_sample.yaml new file mode 100644 index 00000000..f7f95dee --- /dev/null +++ b/testsuites/kubestone/samples/deployment_sample.yaml @@ -0,0 +1,21 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx +spec: + replicas: 3 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.7.9 + ports: + - containerPort: 80 diff --git a/testsuites/kubestone/samples/pod_sample.yaml b/testsuites/kubestone/samples/pod_sample.yaml new file mode 100644 index 00000000..3035cbb2 --- /dev/null +++ b/testsuites/kubestone/samples/pod_sample.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Pod +metadata: + name: myapp-pod + labels: + app: myapp +spec: + containers: + - name: myapp-container + image: busybox + command: ['sh', '-c', 'echo Hello Kubernetes! && sleep 3600'] diff --git a/testsuites/kubestone/stress_test.py b/testsuites/kubestone/stress_test.py new file mode 100644 index 00000000..7f5d75fb --- /dev/null +++ b/testsuites/kubestone/stress_test.py @@ -0,0 +1,156 @@ +from kubernetes import client, config +from utils.k8s_setup import k8s_utils + +import os +import datetime +import uuid + +import utils.logger as log +import yaml +import argparse + +LOG = log.Logger(__name__).getLogger() + +parser = argparse.ArgumentParser(description='kubestone (k8s stress) tests') +parser.add_argument("-c", "--TEST_CASE", + help="The path of test case in form of yaml") +args = parser.parse_args() +TEST_CASE = args.TEST_CASE +TEST_CASE_NAME, TEST_CASE_FORMAT = os.path.splitext( + os.path.basename(TEST_CASE)) +OUT_FILE = ("/tmp/bottlenecks_kubestone_" + + TEST_CASE_NAME + "_" + str(uuid.uuid4()) + ".out") + + +def test_step_result(num, success_num, during_seconds, result): + testdata = {} + test_result = {} + test_result["number_of_deployments"] = float(num) + test_result["success_deployments"] = success_num + test_result["success_rate"] = success_num / num + test_result["duration_time"] = during_seconds + test_result["result"] = result + testdata["data_body"] = test_result + testdata["testcase"] = TEST_CASE_NAME + return testdata + + +def main(): + INSTALLER_TYPE = os.getenv("INSTALLER_TYPE") + K8S_CONFIG_PATH = os.getenv("K8S_CONFIG_PATH") + K8S_APPS_API_VERSION = os.getenv("K8S_APPS_API_VERSION") + K8S_CORE_API_VERSION = os.getenv("K8S_CORE_API_VERSION") + # Get k8s config. If provided in the path indicated by + # K8S_CONFIG_PATH, only return the path. + if K8S_CONFIG_PATH: + k8s_utils.get_config_path( + K8S_CONFIG_PATH=K8S_CONFIG_PATH) + else: + if INSTALLER_TYPE: + K8S_CONFIG_PATH = k8s_utils.get_config_path( + INSTALLER_TYPE=INSTALLER_TYPE) + else: + k8s_utils.get_config_path() + + config.load_kube_config(K8S_CONFIG_PATH) + + # Initiate api clients + if K8S_APPS_API_VERSION: + apps_api = k8s_utils.get_apps_api(K8S_APPS_API_VERSION) + else: + apps_api = k8s_utils.get_apps_api() + + if K8S_CORE_API_VERSION: + core_api = k8s_utils.get_core_api(K8S_CORE_API_VERSION) + else: + core_api = k8s_utils.get_core_api() + + # Read test case in the form of yaml + with open(TEST_CASE) as test_case_file: + test_case_yaml = yaml.load(test_case_file) + if test_case_yaml['template']: + if test_case_yaml['template'].lower() == 'none': + deployment_yaml = test_case_yaml + else: + with open(test_case_yaml['template']) as deployment_file: + deployment_yaml = yaml.load(deployment_file) + else: + deployment_yaml = test_case_yaml + + name = deployment_yaml['metadata']['name'] + namespace = deployment_yaml['namespace'] + body = client.V1Deployment() + body.api_version = deployment_yaml['apiVersion'] + body.kind = deployment_yaml['kind'] + body.metadata = deployment_yaml['metadata'] + body.spec = deployment_yaml['spec'] + pretty = True + + # Create namespace + namespace_existed = k8s_utils.get_namespace_status(namespace) + if namespace_existed[0] == 0 and \ + 'exception' not in namespace_existed[1].lower(): + namespace_read = core_api.read_namespace(namespace, pretty=pretty) + LOG.info('Namespace {} already exist: \n{}'.format( + namespace, namespace_read)) + else: + namespace_body = client.V1Namespace() + namespace_body.metadata = {'name': namespace} + namespace_created = core_api.create_namespace( + namespace_body, pretty=pretty) + LOG.info('Namespace has been created:\n{}'.format( + namespace_created)) + + # Create deployment + deployment_existed = k8s_utils.get_deployment_status(name, namespace) + if deployment_existed[0] == 0 and \ + 'exception' not in deployment_existed[1].lower(): + deployment_read = apps_api.read_namespaced_deployment( + name, namespace, pretty=pretty) + LOG.info('Deployment {}@{} already exist.'.format(name, namespace)) + LOG.info('Discription of this deployment is:\n{}'.format( + deployment_read)) + else: + deployment_created = apps_api.create_namespaced_deployment( + namespace, body, pretty=pretty) + LOG.info('Deployment has been created:\n{}'.format( + deployment_created)) + + # Scale the deployment + scaling_steps = deployment_yaml['scaling_steps'].split(',') + for step in scaling_steps: + start_time = datetime.datetime.now() + + step = int(step) + body.spec['replicas'] = step + api_response = apps_api.patch_namespaced_deployment_scale( + name, namespace, body, pretty=pretty) + LOG.info("Deployment replicas is to be scaled to: %s" % step) + pods_number = k8s_utils.get_available_pods(name, namespace) + while pods_number != step: + pods_number = k8s_utils.get_available_pods(name, namespace) + LOG.info("Number of available pods are {} out of {}".format( + pods_number, step)) + api_response = apps_api.read_namespaced_deployment_scale( + name, namespace, pretty=pretty) + LOG.info( + "Deployment {}-scaling finished:\n{}".format( + step, api_response)) + + end_time = datetime.datetime.now() + duration_seconds = (start_time - end_time).seconds + if pods_number == step: + criteria = 'PASS' + else: + criteria = 'FAIL' + test_result_body = test_step_result( + step, pods_number, duration_seconds, criteria) + k8s_utils.write_json(test_result_body, OUT_FILE) + if api_response: + LOG.info("Deployment scaling test has been successfuly executed.") + LOG.info("Testing results written in: {}".format(OUT_FILE)) + return + + +if __name__ == '__main__': + main() diff --git a/testsuites/kubestone/testcases/__init__.py b/testsuites/kubestone/testcases/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/testsuites/kubestone/testcases/deployment_capacity.yaml b/testsuites/kubestone/testcases/deployment_capacity.yaml new file mode 100644 index 00000000..2638211a --- /dev/null +++ b/testsuites/kubestone/testcases/deployment_capacity.yaml @@ -0,0 +1,25 @@ +apiVersion: apps/v1 +kind: Deployment +namespace: bottlenecks-kubestone +test_type: Horizontal-Scaling +scaling_steps: 10, 50, 100, 200 +template: None +metadata: + name: nginx-deployment + labels: + app: nginx +spec: + replicas: 3 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.7.9 + ports: + - containerPort: 80 diff --git a/utils/k8s_setup/k8s_config_pre.sh b/utils/k8s_setup/k8s_config_pre.sh index f41ba78d..05c3f1c3 100644 --- a/utils/k8s_setup/k8s_config_pre.sh +++ b/utils/k8s_setup/k8s_config_pre.sh @@ -7,7 +7,7 @@ # which accompanies this distribution, and is available at # http://www.apache.org/licenses/LICENSE-2.0 ############################################################################## -K8S_CONFIG="/tmp/k8s_conig" +K8S_CONFIG="/tmp/k8s_config" usage="Script to prepare kubenetes test configurations. diff --git a/utils/k8s_setup/k8s_utils.py b/utils/k8s_setup/k8s_utils.py index 7195bf23..a89889f3 100644 --- a/utils/k8s_setup/k8s_utils.py +++ b/utils/k8s_setup/k8s_utils.py @@ -9,25 +9,29 @@ ############################################################################## import os +import commands +import json import utils.logger as log from kubernetes import client, watch LOG = log.Logger(__name__).getLogger() INSTALLER_TYPE = os.getenv("INSTALLER_TYPE") +K8S_UTILS = "/home/opnfv/bottlenecks/utils/k8s_setup" -def get_config_path(INSTALLER_TYPE=None, CONFIG_PATH="/tmp/k8s_config"): +def get_config_path(INSTALLER_TYPE=None, K8S_CONFIG_PATH="/tmp/k8s_config"): if INSTALLER_TYPE: - CMD = "bash k8s_config_pre.sh -i " + INSTALLER_TYPE + \ - " -c " + CONFIG_PATH + CMD = "bash " + K8S_UTILS + "/k8s_config_pre.sh -i " \ + + INSTALLER_TYPE + \ + " -c " + K8S_CONFIG_PATH LOG.info("Executing command: " + CMD) - CONFIG_PATH = os.popen(CMD) + os.popen(CMD) else: - if not os.path.exists(CONFIG_PATH): + if not os.path.exists(K8S_CONFIG_PATH): raise Exception("Must at least specify the path \ of k8s config!") - return CONFIG_PATH + return K8S_CONFIG_PATH def get_core_api(version='v1'): @@ -35,10 +39,39 @@ def get_core_api(version='v1'): API = client.CoreV1Api() LOG.info(API) else: - raise Exception("Must input a validate verison!") + raise Exception("Must input a valid verison!") return API +def get_apps_api(version='v1'): + if version.lower() == 'v1': + API = client.AppsV1Api() + LOG.info(API) + else: + raise Exception("Must input a valid verison!") + return API + + +def get_namespace_status(namespace): + CMD = ("kubectl get ns | grep %s" % namespace) + namespace_existed = commands.getstatusoutput(CMD) + return namespace_existed + + +def get_deployment_status(name, namespace): + CMD = ("kubectl get deployment --namespace={} | grep {}".format( + namespace, name)) + deployment_existed = commands.getstatusoutput(CMD) + return deployment_existed + + +def get_available_pods(name, namespace): + CMD = ("kubectl get deployment --namespace={} | grep {}".format( + namespace, name) + " | awk '{print $5}'") + available_pods = commands.getstatusoutput(CMD) + return int(available_pods[1]) + + def watch_namespace(namespace, count=3, stop=None, request_timeout=0): w = watch.Watch() LOG.debug("Watch object generated: {}".format(w)) @@ -54,3 +87,9 @@ def watch_namespace(namespace, count=3, stop=None, request_timeout=0): if not count: LOG.info("Ended.\n") w.stop() + + +def write_json(data, file_name): + with open(file_name, "a") as f: + f.write(json.dumps(data, f)) + f.write("\n") -- cgit 1.2.3-korg