diff options
Diffstat (limited to 'src/test/behave_tests/features')
-rw-r--r-- | src/test/behave_tests/features/ceph_osd_test.feature | 49 | ||||
-rw-r--r-- | src/test/behave_tests/features/ceph_shell_test.feature | 64 | ||||
-rw-r--r-- | src/test/behave_tests/features/cephadm_test.feature | 24 | ||||
-rw-r--r-- | src/test/behave_tests/features/environment.py | 207 | ||||
-rw-r--r-- | src/test/behave_tests/features/kcli_handler.py | 88 | ||||
-rw-r--r-- | src/test/behave_tests/features/steps/ceph_steps.py | 106 | ||||
-rw-r--r-- | src/test/behave_tests/features/validation_util.py | 19 |
7 files changed, 557 insertions, 0 deletions
diff --git a/src/test/behave_tests/features/ceph_osd_test.feature b/src/test/behave_tests/features/ceph_osd_test.feature new file mode 100644 index 000000000..e9a37a4c9 --- /dev/null +++ b/src/test/behave_tests/features/ceph_osd_test.feature @@ -0,0 +1,49 @@ +@osd +Feature: Tests related to OSD creation + In order to be able to provide storage services + As an system administrator + I want to install a Ceph cluster in the following server infrastructure: + - 3 nodes with 8Gb RAM, 4 CPUs, and 3 storage devices of 20Gb each. + - Using Fedora32 image in each node + - Configure ceph cluster in following way + - with number of OSD 0 + + + Scenario: Create OSDs + Given I log as root into ceph-node-00 + When I execute in cephadm_shell + """ + ceph orch device ls + """ + Then I wait for 60 seconds until I get + """ + ceph-node-00.cephlab.com /dev/vdb hdd Unknown N/A N/A Yes + ceph-node-01.cephlab.com /dev/vdb hdd Unknown N/A N/A Yes + ceph-node-02.cephlab.com /dev/vdb hdd Unknown N/A N/A Yes + """ + Then I execute in cephadm_shell + """ + ceph orch daemon add osd ceph-node-00.cephlab.com:/dev/vdb + ceph orch daemon add osd ceph-node-01.cephlab.com:/dev/vdb + ceph orch daemon add osd ceph-node-02.cephlab.com:/dev/vdb + """ + Then I execute in cephadm_shell + """ + ceph orch device ls + """ + Then I wait for 60 seconds until I get + """ + ceph-node-00.cephlab.com /dev/vdb hdd Unknown N/A N/A No + ceph-node-01.cephlab.com /dev/vdb hdd Unknown N/A N/A No + ceph-node-02.cephlab.com /dev/vdb hdd Unknown N/A N/A No + """ + Then I execute in cephadm_shell + """ + ceph -s + """ + Then I get results which contain + """ + services: + mon: 3 daemons, quorum ceph-node-00.cephlab.com,ceph-node-01,ceph-node-02 + osd: 3 osds: 3 up + """ diff --git a/src/test/behave_tests/features/ceph_shell_test.feature b/src/test/behave_tests/features/ceph_shell_test.feature new file mode 100644 index 000000000..b158093a0 --- /dev/null +++ b/src/test/behave_tests/features/ceph_shell_test.feature @@ -0,0 +1,64 @@ +@ceph_shell +Feature: Testing basic ceph shell commands + In order to be able to provide storage services + As an system administrator + I want to install a Ceph cluster in the following server infrastructure: + - 3 nodes with 8Gb RAM, 4 CPUs, and 3 storage devices of 20Gb each. + - Using Fedora32 image in each node + + + Scenario: Execute ceph command to check status + Given I log as root into ceph-node-00 + When I execute in cephadm_shell + """ + ceph orch status + """ + Then I get results which contain + """ + Backend: cephadm + Available: Yes + Paused: No + """ + + + Scenario: Execute ceph command to check orch host list + Given I log as root into ceph-node-00 + When I execute in cephadm_shell + """ + ceph orch host ls + """ + Then I get results which contain + """ + HOST LABELS + ceph-node-00.cephlab.com _admin + """ + + + Scenario: Execute ceph command to check orch device list + Given I log as root into ceph-node-00 + When I execute in cephadm_shell + """ + ceph orch device ls + """ + Then I get results which contain + """ + Hostname Path Type + ceph-node-00.cephlab.com /dev/vdb hdd + ceph-node-00.cephlab.com /dev/vdc hdd + """ + + + Scenario: Execute ceph command to check orch + Given I log as root into ceph-node-00 + When I execute in cephadm_shell + """ + ceph orch ls + """ + Then I wait for 60 seconds until I get + """ + NAME RUNNING + grafana 1/1 + mgr 2/2 + mon 1/5 + prometheus 1/1 + """ diff --git a/src/test/behave_tests/features/cephadm_test.feature b/src/test/behave_tests/features/cephadm_test.feature new file mode 100644 index 000000000..e3358bfbd --- /dev/null +++ b/src/test/behave_tests/features/cephadm_test.feature @@ -0,0 +1,24 @@ +@cephadm +Feature: Install a basic Ceph cluster + In order to be able to provide storage services + As an system administrator + I want to install a Ceph cluster in the following server infrastructure: + - 3 nodes with 8Gb RAM, 4 CPUs, and 3 storage devices of 20Gb each. + - Using Fedora32 image in each node + + + Scenario: Execute commands in cluster nodes + Given I log as root into ceph-node-00 + And I execute in host + """ + curl --silent --remote-name --location https://raw.githubusercontent.com/ceph/ceph/octopus/src/cephadm/cephadm + chmod +x cephadm + """ + When I execute in host + """ + cephadm version + """ + Then I get results which contain + """ + ceph version quincy (dev) + """ diff --git a/src/test/behave_tests/features/environment.py b/src/test/behave_tests/features/environment.py new file mode 100644 index 000000000..fdd175e60 --- /dev/null +++ b/src/test/behave_tests/features/environment.py @@ -0,0 +1,207 @@ +import logging +import os +import re + +from jinja2 import Template +from kcli_handler import is_bootstrap_script_complete, execute_kcli_cmd + +KCLI_PLANS_DIR = "generated_plans" +KCLI_PLAN_NAME = "behave_test_plan" + +Kcli_Config = { + "nodes": 1, + "pool": "default", + "network": "default", + "domain": "cephlab.com", + "prefix": "ceph", + "numcpus": 1, + "memory": 1024, + "image": "fedora33", + "notify": False, + "admin_password": "password", + "disks": [150, 3], +} + +Bootstrap_Config = { + "configure_osd": False +} + + +def _write_file(file_path, data): + with open(file_path, "w") as file: + file.write(data) + + +def _read_file(file_path): + file = open(file_path, "r") + data = "".join(file.readlines()) + file.close() + return data + + +def _loaded_templates(): + temp_dir = os.path.join(os.getcwd(), "template") + logging.info("Loading templates") + kcli = _read_file(os.path.join(temp_dir, "kcli_plan_template")) + script = _read_file(os.path.join(temp_dir, "bootstrap_script_template")) + return ( + Template(kcli), + Template(script) + ) + + +def _clean_generated(dir_path): + logging.info("Deleting generated files") + for file in os.listdir(dir_path): + os.remove(os.path.join(dir_path, file)) + os.rmdir(dir_path) + + +def _parse_value(value): + if value.isnumeric(): + return int(value) + + if value.endswith("gb"): + return int(value.replace("gb", "")) * 1024 + elif value.endswith("mb"): + return value.replace("mb", "") + return value + + +def _parse_to_config_dict(values, config): + for key in values.keys(): + config[key] = _parse_value(values[key]) + + +def _parse_vm_description(specs): + """ + Parse's vm specfication description into configuration dictionary + """ + kcli_config = Kcli_Config.copy() + parsed_str = re.search( + r"(?P<nodes>[\d]+) nodes with (?P<memory>[\w\.-]+) ram", + specs.lower(), + ) + if parsed_str: + for spec_key in parsed_str.groupdict().keys(): + kcli_config[spec_key] = _parse_value(parsed_str.group(spec_key)) + parsed_str = re.search(r"(?P<numcpus>[\d]+) cpus", specs.lower()) + if parsed_str: + kcli_config["numcpus"] = parsed_str.group("numcpus") + parsed_str = re.search( + r"(?P<disk>[\d]+) storage devices of (?P<volume>[\w\.-]+)Gb each", + specs, + ) + if parsed_str: + kcli_config["disks"] = [ + _parse_value(parsed_str.group("volume")) + ] * _parse_value(parsed_str.group("disk")) + parsed_str = re.search(r"(?P<image>[\w\.-]+) image", specs.lower()) + if parsed_str: + kcli_config["image"] = parsed_str.group("image") + return kcli_config + + +def _parse_ceph_description(specs): + """ + Parse the ceph boostrap script configuration descriptions. + """ + bootstrap_script_config = Bootstrap_Config.copy() + parsed_str = re.search( + r"OSD (?P<osd>[\w\.-]+)", specs + ) + if parsed_str: + bootstrap_script_config["configure_osd"] = True if _parse_value( + parsed_str.group("osd") + ) else False + return bootstrap_script_config + + +def _handle_kcli_plan(command_type, plan_file_path=None): + """ + Executes the kcli vm create and delete command according + to the provided configuration. + """ + op = None + if command_type == "create": + # TODO : Before creating kcli plan check for exisitng kcli plans + op, code = execute_kcli_cmd( + f"create plan -f {plan_file_path} {KCLI_PLAN_NAME}" + ) + if code: + print(f"Failed to create kcli plan\n Message: {op}") + exit(1) + elif command_type == "delete": + op, code = execute_kcli_cmd(f"delete plan {KCLI_PLAN_NAME} -y") + print(op) + + +def has_ceph_configuration(descriptions, config_line): + """ + Checks for ceph cluster configuration in descriptions. + """ + index_config = -1 + for line in descriptions: + if line.lower().startswith(config_line): + index_config = descriptions.index(line) + + if index_config != -1: + return ( + descriptions[:index_config], + descriptions[index_config:], + ) + return ( + descriptions, + None, + ) + + +def before_feature(context, feature): + kcli_plans_dir_path = os.path.join( + os.getcwd(), + KCLI_PLANS_DIR, + ) + if not os.path.exists(kcli_plans_dir_path): + os.mkdir(kcli_plans_dir_path) + + vm_description, ceph_description = has_ceph_configuration( + feature.description, + "- configure ceph cluster", + ) + loaded_kcli, loaded_script = _loaded_templates() + + vm_feature_specs = " ".join( + [line for line in vm_description if line.startswith("-")] + ) + vm_config = _parse_vm_description("".join(vm_feature_specs)) + kcli_plan_path = os.path.join(kcli_plans_dir_path, "gen_kcli_plan.yml") + print(f"Kcli vm configureaton \n {vm_config}") + _write_file( + kcli_plan_path, + loaded_kcli.render(vm_config) + ) + + # Checks for ceph description if None set the default configurations + ceph_config = _parse_ceph_description( + "".join(ceph_description) + ) if ceph_description else Bootstrap_Config + + print(f"Bootstrap configuraton \n {ceph_config}\n") + _write_file( + os.path.join(kcli_plans_dir_path, "bootstrap_cluster_dev.sh"), + loaded_script.render(ceph_config), + ) + + _handle_kcli_plan("create", os.path.relpath(kcli_plan_path)) + + if not is_bootstrap_script_complete(): + print("Failed to complete bootstrap..") + _handle_kcli_plan("delete") + exit(1) + context.last_executed = {} + + +def after_feature(context, feature): + if os.path.exists(KCLI_PLANS_DIR): + _clean_generated(os.path.abspath(KCLI_PLANS_DIR)) + _handle_kcli_plan("delete") diff --git a/src/test/behave_tests/features/kcli_handler.py b/src/test/behave_tests/features/kcli_handler.py new file mode 100644 index 000000000..1e28c7ff4 --- /dev/null +++ b/src/test/behave_tests/features/kcli_handler.py @@ -0,0 +1,88 @@ +import subprocess +import time +import os + + +kcli_exec = r""" +podman run --net host -it --rm --security-opt label=disable + -v $HOME/.ssh:/root/.ssh -v $HOME/.kcli:/root/.kcli + -v /var/lib/libvirt/images:/var/lib/libvirt/images + -v /var/run/libvirt:/var/run/libvirt -v $PWD:/workdir + -v /var/tmp:/ignitiondir jolmomar/kcli +""" + + +def _create_kcli_cmd(command): + cmd = kcli_exec.replace("$HOME", os.getenv("HOME")) + cmd = cmd.replace("$PWD", os.getenv("PWD")) + kcli = cmd.replace("\n", "").split(" ") + return kcli + command.split(" ") + + +def is_bootstrap_script_complete(): + """ + Checks for status of bootstrap script executions. + """ + timeout = 0 + command = " ".join( + [ + f'"{cmd}"' for cmd in + "journalctl --no-tail --no-pager -t cloud-init".split(" ") + ] + ) + cmd = _create_kcli_cmd( + f'ssh ceph-node-00 {command} | grep "Bootstrap complete."' + ) + while timeout < 10: # Totally waits for 5 mins before giving up + proc = subprocess.run(cmd, capture_output=True, text=True) + if "Bootstrap complete." in proc.stdout: + print("Bootstrap script completed successfully") + return True + timeout += 1 + print("Waiting for bootstrap_cluster script...") + print(proc.stdout[len(proc.stdout) - 240:]) + time.sleep(30) + print( + f"Timeout reached {30*timeout}. Giving up for boostrap to complete" + ) + return False + + +def execute_kcli_cmd(command): + """ + Executes the kcli command by combining the provided command + with kcli executable command. + """ + cmd = _create_kcli_cmd(command) + print(f"Executing kcli command : {command}") + try: + proc = subprocess.run( + cmd, + capture_output=True, + text=True, + # env=dict(STORAGE_OPTS=''), + ) + except Exception as ex: + print(f"Error executing kcli command\n{ex}") + + op = proc.stderr if proc.stderr else proc.stdout + return (op, proc.returncode) + + +def execute_ssh_cmd(vm_name, shell, command): + """ + Executes the provided ssh command on the provided vm machine + """ + if shell == "cephadm_shell": + command = f"cephadm shell {command}" + sudo_cmd = f"sudo -i {command}".split(" ") + sudo_cmd = " ".join([f'"{cmd}"' for cmd in sudo_cmd]) + cmd = _create_kcli_cmd(f"ssh {vm_name} {sudo_cmd}") + print(f"Executing ssh command : {cmd}") + try: + proc = subprocess.run(cmd, capture_output=True, text=True) + except Exception as ex: + print(f"Error executing ssh command: {ex}") + + op = proc.stderr if proc.stderr else proc.stdout + return (op, proc.returncode) diff --git a/src/test/behave_tests/features/steps/ceph_steps.py b/src/test/behave_tests/features/steps/ceph_steps.py new file mode 100644 index 000000000..a96aa48ad --- /dev/null +++ b/src/test/behave_tests/features/steps/ceph_steps.py @@ -0,0 +1,106 @@ +import time + +from behave import given, when, then +from kcli_handler import execute_ssh_cmd +from validation_util import str_to_list + + +@given("I log as root into {node}") +def login_to_node(context, node): + context.node = node + + +@given("I execute in {shell}") +def init_step_execute(context, shell): + commands = context.text.split("\n") + for command in commands: + op, code = execute_ssh_cmd(context.node, shell, command) + if code: + raise Exception("Failed to execute") + context.last_executed["cmd"] = command + context.last_executed["shell"] = shell + + +@when("I execute in {shell}") +@then("I execute in {shell}") +def execute_step(context, shell): + if context.node is None: + raise Exception("Failed not logged into virtual machine") + for command in context.text.split("\n"): + output, return_code = execute_ssh_cmd(context.node, shell, command) + context.last_executed["cmd"] = command + context.last_executed["shell"] = shell + if return_code != 0: + raise Exception(f"Failed to execute ssh\n Message:{output}") + context.output = str_to_list(output) + print(f"Executed output : {context.output}") + + +@then("Execute in {shell} only {command}") +def execute_only_one_step(context, shell, command): + """ + Run's single command and doesn't use multi-line + :params command: given command to execute + """ + if context.node is None: + raise Exception("Failed not logged into virtual machine") + output, return_code = execute_ssh_cmd(context.node, shell, command) + context.last_executed["cmd"] = command + context.last_executed["shell"] = shell + if return_code != 0: + raise Exception(f"Failed to execute ssh\nMessage:{output}") + context.output = str_to_list(output) + print(f"Executed output : {context.output}") + + +@then("I wait for {time_out:n} seconds until I get") +def execute_and_wait_until_step(context, time_out): + wait_time = int(time_out/4) + context.found_all_keywords = False + if context.node is None: + raise Exception("Failed not logged into virtual machine") + exec_shell = context.last_executed['shell'] + exec_cmd = context.last_executed['cmd'] + if exec_shell is None and exec_cmd is None: + raise Exception("Last executed command not found..") + + expected_output = str_to_list(context.text) + while wait_time < time_out and not context.found_all_keywords: + found_keys = [] + context.execute_steps( + f"then Execute in {exec_shell} only {exec_cmd}" + ) + + executed_output = context.output + for expected_line in expected_output: + for op_line in executed_output: + if set(expected_line).issubset(set(op_line)): + found_keys.append(" ".join(expected_line)) + + if len(found_keys) != len(expected_output): + print(f"Waiting for {int(time_out/4)} seconds") + time.sleep(int(time_out/4)) + wait_time += int(time_out/4) + else: + print("Found all expected keywords") + context.found_all_keywords = True + break + if not context.found_all_keywords: + print( + f"Timeout reached {time_out}. Giving up on waiting for keywords" + ) + + +@then("I get results which contain") +def validation_step(context): + expected_keywords = str_to_list(context.text) + output_lines = context.output + + for keys_line in expected_keywords: + found_keyword = False + for op_line in output_lines: + if set(keys_line).issubset(set(op_line)): + found_keyword = True + output_lines.remove(op_line) + if not found_keyword: + assert False, f"Not found {keys_line}" diff --git a/src/test/behave_tests/features/validation_util.py b/src/test/behave_tests/features/validation_util.py new file mode 100644 index 000000000..abe441462 --- /dev/null +++ b/src/test/behave_tests/features/validation_util.py @@ -0,0 +1,19 @@ + +def str_to_list(string): + """ + Converts the string into list removing whitespaces + """ + string = string.replace('\t', '\n') + return [ + [ + key for key in line.split(' ') + if key != '' + ] + for line in string.split('\n') + if line != '' + ] + + +def assert_str_in_list(keyword_list, output_list): + for keyword in keyword_list: + assert keyword in output_list, f" Not found {keyword}" |