diff --git a/keywords/files/yaml_keywords.py b/keywords/files/yaml_keywords.py index eafe13e0..e3a4d3e2 100644 --- a/keywords/files/yaml_keywords.py +++ b/keywords/files/yaml_keywords.py @@ -67,3 +67,17 @@ class YamlKeywords(BaseKeyword): FileKeywords(self.ssh_connection).upload_file(rendered_yaml_file_location, target_remote_file) return target_remote_file return rendered_yaml_file_location + + def load_yaml(self, file_path): + """ + This function will load a yaml file from the local dir + + Args: + file_path (str): Path to the YAML file. + Example: 'resources/cloud_platform/folder/file_name'. + + Returns: + file: The loaded YAML file + """ + with open(file_path, 'r') as file: + return yaml.safe_load(file) \ No newline at end of file diff --git a/keywords/openstack/command_wrappers.py b/keywords/openstack/command_wrappers.py new file mode 100644 index 00000000..34ae457d --- /dev/null +++ b/keywords/openstack/command_wrappers.py @@ -0,0 +1,5 @@ +def source_admin_openrc(cmd: str): + return f"source /var/opt/openstack/admin-openrc;/var/opt/openstack/clients-wrapper.sh {cmd}" + +def source_sudo_admin_openrc(cmd: str): + return f"bash -c '/var/opt/openstack/admin-openrc;/var/opt/openstack/clients-wrapper.sh {cmd}'" diff --git a/keywords/openstack/openstack/openstack_json_parser.py b/keywords/openstack/openstack/openstack_json_parser.py new file mode 100644 index 00000000..79898a90 --- /dev/null +++ b/keywords/openstack/openstack/openstack_json_parser.py @@ -0,0 +1,29 @@ +import json + +class OpenstackJsonParser: + """ + Class for Openstack json parsing + + Sample JSON: + { + "ID": "1bb26a3c-5a7a-4eb4-8c70-73ef4b93327d", + "Stack Name": "stack_test", + "Project": "f42e14df49524f8eb1fd0665b21122cd", + "Stack Status": "CREATE_COMPLETE", + "Creation Time": "2025-04-10T12:31:03Z", + "Updated Time": null + } + """ + + def __init__(self, system_output): + openstack_stack_output_string = ''.join(system_output) + json_part = openstack_stack_output_string.split('\n', 1)[1] + self.system_output = json_part + + def get_output_values_list(self): + """ + Getter for output values list + Returns: the output values list + + """ + return json.loads(self.system_output) diff --git a/keywords/openstack/openstack/openstack_replacement_dict_parser.py b/keywords/openstack/openstack/openstack_replacement_dict_parser.py new file mode 100644 index 00000000..8d6b3913 --- /dev/null +++ b/keywords/openstack/openstack/openstack_replacement_dict_parser.py @@ -0,0 +1,29 @@ +import json +from keywords.openstack.openstack.stack.object.openstack_stack_heat_default_enum import OpenstackStackHeatDefaultEnum + +class OpenstackReplacementDictParser: + """ + Class for Auxiliary Replacement dictionary for helping with the manage stack + """ + + def __init__(self, config, default_json_name): + self.config = config + self.default_json_name = getattr(OpenstackStackHeatDefaultEnum, default_json_name.upper()) + self.default_json = json.loads(self.default_json_name.value) + self.replacement_dict = {} + self.keys_to_exclude = OpenstackStackHeatDefaultEnum.KEEP_JSON.value + + self.flatten_dict(self.default_json) + + def flatten_dict(self, d): + for key, value in d.items(): + if isinstance(value, dict) and key not in self.keys_to_exclude: + self.flatten_dict(value) + else: + if key in self.config: + self.replacement_dict[key] = self.config[key] + else: + self.replacement_dict[key] = value + + def get_replacement_dict(self): + return self.replacement_dict \ No newline at end of file diff --git a/keywords/openstack/openstack/stack/object/openstack_manage_stack_create_input.py b/keywords/openstack/openstack/stack/object/openstack_manage_stack_create_input.py new file mode 100644 index 00000000..8a5ca970 --- /dev/null +++ b/keywords/openstack/openstack/stack/object/openstack_manage_stack_create_input.py @@ -0,0 +1,32 @@ +class OpenstackManageStackCreateInput: + """ + Class to support the parameters for managing OpenStack stacks. + """ + + def __init__(self): + self.resource_list = None + self.file_destination = None + + def set_resource_list(self, resource_list: dict): + """ + Setter for the 'resource_list' property. + """ + self.resource_list = resource_list + + def get_resource_list(self) -> dict: + """ + Getter for the 'resource_list' property. + """ + return self.resource_list + + def set_file_destination(self, file_destination: str): + """ + Setter for the 'file_destination' property. + """ + self.file_destination = file_destination + + def get_file_destination(self) -> str: + """ + Getter for the 'file_destination' property. + """ + return self.file_destination diff --git a/keywords/openstack/openstack/stack/object/openstack_manage_stack_delete_input.py b/keywords/openstack/openstack/stack/object/openstack_manage_stack_delete_input.py new file mode 100644 index 00000000..67e1b93d --- /dev/null +++ b/keywords/openstack/openstack/stack/object/openstack_manage_stack_delete_input.py @@ -0,0 +1,32 @@ +class OpenstackManageStackDeleteInput: + """ + Class to support the parameters for managing OpenStack stacks. + """ + + def __init__(self): + self.resource_list = None + self.skip_list = [] + + def set_resource_list(self, resource_list: dict): + """ + Setter for the 'resource_list' property. + """ + self.resource_list = resource_list + + def get_resource_list(self) -> dict: + """ + Getter for the 'resource_list' property. + """ + return self.resource_list + + def set_skip_list(self, skip_list: list): + """ + Setter for the 'skip_list' property. + """ + self.skip_list = skip_list + + def get_skip_list(self) -> list: + """ + Getter for the 'skip_list' property. + """ + return self.skip_list diff --git a/keywords/openstack/openstack/stack/object/openstack_stack_create_input.py b/keywords/openstack/openstack/stack/object/openstack_stack_create_input.py new file mode 100644 index 00000000..d1fd43a4 --- /dev/null +++ b/keywords/openstack/openstack/stack/object/openstack_stack_create_input.py @@ -0,0 +1,66 @@ +class OpenstackStackCreateInput: + """ + Class to support the parameters for 'openstack stack create' command. + An example of using this command is: + + 'openstack stack create --template=file/location/stack.yaml' + + This class is able to generate this command just by previously setting the parameters. + """ + + def __init__(self): + """ + Constructor + """ + self.stack_name = None + self.template_file_name = None + self.template_file_path = 'heat' + self.return_format = 'json' + + def set_stack_name(self, stack_name: str): + """ + Setter for the 'stack_name' parameter. + """ + self.stack_name = stack_name + + def get_stack_name(self) -> str: + """ + Getter for this 'stack_name' parameter. + """ + return self.stack_name + + def set_template_file_name(self, template_file_name: str): + """ + Setter for the 'template_file_name' parameter. + """ + self.template_file_name = template_file_name + + def get_template_file_name(self) -> str: + """ + Getter for this 'template_file_name' parameter. + """ + return self.template_file_name + + def set_template_file_path(self, template_file_path: str): + """ + Setter for the 'template_file_path' parameter. + """ + self.template_file_path = template_file_path + + def get_template_file_path(self) -> str: + """ + Getter for this 'template_file_path' parameter. + """ + return self.template_file_path + + def set_return_format(self, return_format: str) -> str: + """ + Getter for this 'get_return_format' parameter. + """ + self.return_format = return_format + + def get_return_format(self) -> str: + """ + Getter for this 'return_format' parameter. + """ + return self.return_format \ No newline at end of file diff --git a/keywords/openstack/openstack/stack/object/openstack_stack_delete_input.py b/keywords/openstack/openstack/stack/object/openstack_stack_delete_input.py new file mode 100644 index 00000000..2f06f09a --- /dev/null +++ b/keywords/openstack/openstack/stack/object/openstack_stack_delete_input.py @@ -0,0 +1,30 @@ +class OpenstackStackDeleteInput: + """ + Class to support the parameters for 'openstack stack delete' command. + An example of using this command is: + + 'openstack stack delete hello-kitty' + + Where: + hello-kitty: the name of the application that you want to delete. + + """ + + def __init__(self): + """ + Constructor + """ + self.app_name = None + self.force_deletion = False + + def get_stack_name(self) -> str: + """ + Getter for this 'stack_name' parameter. + """ + return self.stack_name + + def set_stack_name(self, stack_name: str): + """ + Setter for the 'stack_name' parameter. + """ + self.stack_name = stack_name diff --git a/keywords/openstack/openstack/stack/object/openstack_stack_heat_default_enum.py b/keywords/openstack/openstack/stack/object/openstack_stack_heat_default_enum.py new file mode 100644 index 00000000..53d28cee --- /dev/null +++ b/keywords/openstack/openstack/stack/object/openstack_stack_heat_default_enum.py @@ -0,0 +1,84 @@ +from enum import Enum +import json + +class OpenstackStackHeatDefaultEnum(Enum): + KEEP_JSON = ["extra_specs"] + + PROJECT = json.dumps({ + "name": "ace_test_1", + "description": "Ace 1 Project", + "quota": { + "network": 8, + "subnet": 18, + "port": 83, + "floatingip": 10, + "instances": 10, + "cores": 30, + "volumes": 12, + "snapshots": 12 + } + }) + USER = json.dumps({ + "name": "ace_user_1", + "password": "password", + "email": "ace_user_1@noreply.com", + "default_project": "ace_test_1" + }) + ROLE_ASSIGNMENT = json.dumps({ + "name": "ace_one_admin_ace_test_1", + "role": "admin", + "project": "ace_test_1", + "user": "ace_one" + }) + FLAVOR = json.dumps({ + "name": "ace_small", + "ram": 1024, + "disk": 2, + "vcpus": 1, + "extra_specs": { + "hw:mem_page_size": "large" + } + }) + WEBIMAGE = json.dumps({ + "name": "ace_cirros", + "container_format": "bare", + "disk_format": "raw", + "location": "http://download.cirros-cloud.net/0.4.0/cirros-0.4.0-x86_64-disk.img", + "min_disk": 0, + "min_ram": 0, + "visibility": "public" + }) + QOS = json.dumps({ + "name": "external-qos", + "description": "External Network Policy", + "project_name": "", + "max_kbps": "10000", + "max_burst_kbps": "1000", + "dscp_mark": "16" + }) + NETWORK_SEGMENT = json.dumps({ + "name": "ace0-ext0-r0-0", + "network_type": "vlan", + "physical_network": "group0", + "minimum_range": "10", + "maximum_range": "10", + "shared": "true", + "private": "false", + "project_name": "" + }) + NETWORK = json.dumps({ + "name": "external-net0", + "subnet_name": "external-subnet0", + "project_name": "admin", + "qos_policy_name": "external-qos", + "provider_network_type": "vlan", + "provider_physical_network": "group0-ext0", + "shared": "true", + "external": "true", + "gateway_ip": "10.10.85.1", + "enable_dhcp": "false", + "subnet_range": "10.10.85.0/24", + "ip_version": "4", + "allocation_pool_start": "", + "allocation_pool_end": "" + }) diff --git a/keywords/openstack/openstack/stack/object/openstack_stack_list_object.py b/keywords/openstack/openstack/stack/object/openstack_stack_list_object.py new file mode 100644 index 00000000..bb218c77 --- /dev/null +++ b/keywords/openstack/openstack/stack/object/openstack_stack_list_object.py @@ -0,0 +1,76 @@ +class OpenstackStackListObject: + """ + Class to handle the data provided by the 'openstack stack list' command execution. This command generates the + output table shown below, where each object of this class represents a single row in that table. + + +----------+------------+----------+-----------------+----------------------+--------------+ + | ID | Stack Name | Project | Stack Status | Creation Time | Updated Time | + +----------+------------+----------+-----------------+----------------------+--------------+ + | 1bb26a3c | stack_test | a3c1bb26 | CREATE_COMPLETE | 2025-04-10T12:31:03Z | None | + +----------+------------+----------+-----------------+----------------------+--------------+ + + """ + + def __init__( + self, + id: str, + stack_name: str, + project: str, + stack_status: str, + creation_time: str, + updated_time: str, + ): + self.id = id + self.stack_name = stack_name + self.project = project + self.stack_status = stack_status + self.creation_time = creation_time + self.updated_time = updated_time + + def get_id(self) -> str: + """ + Getter for id + Returns: the id + + """ + return self.id + + def get_stack_name(self) -> str: + """ + Getter for stack name + Returns: the stack name + + """ + return self.stack_name + + def get_project(self) -> str: + """ + Getter for project + Returns: the project + + """ + return self.project + + def get_stack_status(self) -> str: + """ + Getter for stack status + Returns: the stack status + + """ + return self.stack_status + + def get_creation_time(self) -> str: + """ + Getter for creation time + Returns: the creation time + + """ + return self.creation_time + + def get_updated_time(self) -> str: + """ + Getter for updated time + Returns: the updated time + + """ + return self.updated_time diff --git a/keywords/openstack/openstack/stack/object/openstack_stack_list_output.py b/keywords/openstack/openstack/stack/object/openstack_stack_list_output.py new file mode 100644 index 00000000..039560f9 --- /dev/null +++ b/keywords/openstack/openstack/stack/object/openstack_stack_list_output.py @@ -0,0 +1,112 @@ +from framework.exceptions.keyword_exception import KeywordException +from framework.logging.automation_logger import get_logger +from keywords.openstack.openstack.stack.object.openstack_stack_list_object import OpenstackStackListObject +from keywords.openstack.openstack.openstack_json_parser import OpenstackJsonParser + + +class OpenstackStackListOutput: + """ + This class parses the output of the command 'openstack stack list' + The parsing result is a 'OpenstackStackListObject' instance. + + Example: + 'openstack stack list' + +----------+------------+----------+-----------------+----------------------+--------------+ + | ID | Stack Name | Project | Stack Status | Creation Time | Updated Time | + +----------+------------+----------+-----------------+----------------------+--------------+ + | 1bb26a3c | stack_test | a3c1bb26 | CREATE_COMPLETE | 2025-04-10T12:31:03Z | None | + +----------+------------+----------+-----------------+----------------------+--------------+ + + """ + + def __init__(self, openstack_stack_list_output): + """ + Constructor + Args: + openstack_stack_list_output: the output of the command 'openstack stack list'. + """ + self.openstack_stacks: [OpenstackStackListObject] = [] + openstack_json_parser = OpenstackJsonParser(openstack_stack_list_output) + output_values = openstack_json_parser.get_output_values_list() + + for value in output_values: + if self.is_valid_output(value): + self.openstack_stacks.append( + OpenstackStackListObject( + value['ID'], + value['Stack Name'], + value['Project'], + value['Stack Status'], + value['Creation Time'], + value['Updated Time'], + ) + ) + else: + raise KeywordException(f"The output line {value} was not valid") + + def get_stacks(self) -> [OpenstackStackListObject]: + """ + Returns the list of stack objects + Returns: + + """ + return self.openstack_stacks + + def get_stack(self, stack_name: str) -> OpenstackStackListObject: + """ + Gets the given stack + Args: + stack_name (): the name of the stack + + Returns: the stack object + + """ + stacks = list(filter(lambda stack: stack.get_stack_name() == stack_name, self.openstack_stacks)) + if len(stacks) == 0: + raise KeywordException(f"No stack with name {stack_name} was found.") + + return stacks[0] + + @staticmethod + def is_valid_output(value): + """ + Checks to ensure the output has the correct keys + Args: + value (): the value to check + + Returns: + + """ + valid = True + if 'ID' not in value: + get_logger().log_error(f'id is not in the output value: {value}') + valid = False + if 'Stack Name' not in value: + get_logger().log_error(f'stack name is not in the output value: {value}') + valid = False + if 'Project' not in value: + get_logger().log_error(f'project is not in the output value: {value}') + valid = False + if 'Stack Status' not in value: + get_logger().log_error(f'stack status is not in the output value: {value}') + valid = False + if 'Creation Time' not in value: + get_logger().log_error(f'creation time is not in the output value: {value}') + valid = False + + return valid + + def is_in_stack_list(self, stack_name: str) -> bool: + """ + Verifies if there is an stack with the name 'stack_name'. + + Args: + stack_name (str): a string representing the stack's name. + + Returns: + bool: True if there is a stack with the name 'stack_name'; False otherwise. + """ + stacks = list(filter(lambda stack: stack.get_stack() == stack_name, self.openstack_stacks)) + if len(stacks) == 0: + return False + return True diff --git a/keywords/openstack/openstack/stack/object/openstack_stack_object.py b/keywords/openstack/openstack/stack/object/openstack_stack_object.py new file mode 100644 index 00000000..d434a221 --- /dev/null +++ b/keywords/openstack/openstack/stack/object/openstack_stack_object.py @@ -0,0 +1,112 @@ +class OpenstackStackObject: + """ + Class to handle data provided by commands such as 'openstack stack create' + + Example: + 'openstack stack create --template=heat/project_stack.yaml stack_test -f json' + { + "id": "1bb26a3c", + "stack_name": "stack_test", + "description": "Heat template to create OpenStack projects.\n", + "creation_time": "2025-04-10T13:35:04Z", + "updated_time": null, + "stack_status": "CREATE_COMPLETE", + "stack_status_reason": "Stack CREATE completed successfully" + } + """ + + def __init__(self): + """ + Constructor. + """ + self.id: str + self.stack_name: str + self.description: str + self.creation_time: str + self.updated_time: str + self.stack_status: str + self.stack_status_reason: str + + def set_id(self, id: str): + """ + Setter for the 'id' property. + """ + self.id = id + + def get_id(self) -> str: + """ + Getter for this 'id' property. + """ + return self.id + + def set_stack_name(self, stack_name: str): + """ + Setter for the 'stack_name' property. + """ + self.stack_name = stack_name + + def get_stack_name(self) -> str: + """ + Getter for the 'stack_name' property. + """ + return self.stack_name + + def set_description(self, description: str): + """ + Setter for the 'description' property. + """ + self.description = description + + def get_description(self) -> str: + """ + Getter for the 'description' property. + """ + return self.description + + def set_creation_time(self, creation_time: str): + """ + Setter for the 'creation_time' property. + """ + self.creation_time = creation_time + + def get_creation_time(self) -> str: + """ + Getter for the 'creation_time' property. + """ + return self.creation_time + + def set_updated_time(self, updated_time: str): + """ + Setter for the 'updated_time' property. + """ + self.updated_time = updated_time + + def get_updated_time(self) -> str: + """ + Getter for the 'updated_time' property. + """ + return self.updated_time + + def set_stack_status(self, stack_status: str): + """ + Setter for the 'stack_status' property. + """ + self.stack_status = stack_status + + def get_stack_status(self) -> str: + """ + Getter for the 'stack_status' property. + """ + return self.stack_status + + def set_stack_status_reason(self, stack_status_reason: str): + """ + Setter for the 'stack_status_reason' property. + """ + self.stack_status_reason = stack_status_reason + + def get_stack_status_reason(self) -> str: + """ + Getter for the 'stack_status_reason' property. + """ + return self.stack_status_reason \ No newline at end of file diff --git a/keywords/openstack/openstack/stack/object/openstack_stack_output.py b/keywords/openstack/openstack/stack/object/openstack_stack_output.py new file mode 100644 index 00000000..7c89378f --- /dev/null +++ b/keywords/openstack/openstack/stack/object/openstack_stack_output.py @@ -0,0 +1,66 @@ +from keywords.openstack.openstack.stack.object.openstack_stack_object import OpenstackStackObject +from keywords.openstack.openstack.openstack_json_parser import OpenstackJsonParser + +class OpenstackStackOutput: + """ + This class parses the output of commands such as 'openstack stack create' + that share the same output as shown in the example below. + The parsing result is a 'openstackStackObject' instance. + + Example: + 'openstack stack create --template=heat/project_stack.yaml stack_test -f json' + { + "id": "1bb26a3c", + "stack_name": "stack_test", + "description": "Heat template to create OpenStack projects.\n", + "creation_time": "2025-04-10T13:35:04Z", + "updated_time": null, + "stack_status": "CREATE_COMPLETE", + "stack_status_reason": "Stack CREATE completed successfully" + } + """ + + def __init__(self, openstack_stack_output): + """ + Constructor. + Create an internal OpenstackStackCreateObject from the passed parameter. + Args: + openstack_stack_output (list[str]): a list of strings representing the output of the + 'openstack stack create' command. + + """ + openstack_json_parser = OpenstackJsonParser(openstack_stack_output) + + output_values = openstack_json_parser.get_output_values_list() + self.openstack_stack_object = OpenstackStackObject() + + if 'id' in output_values: + self.openstack_stack_object.set_id(output_values['id']) + + if 'stack_name' in output_values: + self.openstack_stack_object.set_stack_name(output_values['stack_name']) + + if 'description' in output_values: + self.openstack_stack_object.set_description(output_values['description']) + + if 'creation_time' in output_values: + self.openstack_stack_object.set_creation_time(output_values['creation_time']) + + if 'updated_time' in output_values: + self.openstack_stack_object.set_updated_time(output_values['updated_time']) + + if 'stack_status' in output_values: + self.openstack_stack_object.set_stack_status(output_values['stack_status']) + + if 'stack_status_reason' in output_values: + self.openstack_stack_object.set_stack_status_reason(output_values['stack_status_reason']) + + def get_openstack_stack_object(self) -> OpenstackStackObject: + """ + Getter for OpenstackStackObject object. + + Returns: + A OpenstackStackObject instance representing the output of commands sucha as 'openstack stack create'. + + """ + return self.openstack_stack_object diff --git a/keywords/openstack/openstack/stack/object/openstack_stack_status_enum.py b/keywords/openstack/openstack/stack/object/openstack_stack_status_enum.py new file mode 100644 index 00000000..47c9bf29 --- /dev/null +++ b/keywords/openstack/openstack/stack/object/openstack_stack_status_enum.py @@ -0,0 +1,16 @@ +from enum import Enum + + +class OpenstackStackStatusEnum(Enum): + CREATE_IN_PROGRESS = "create_in_progress" + CREATE_COMPLETE = "create_complete" + CREATE_FAILED = "create_failed" + DELETE_IN_PROGRESS = "delete_in_progress" + DELETE_COMPLETE = "delete_complete" + DELETE_FAILED = "delete_failed" + UPDATE_IN_PROGRESS = "update_in_progress" + UPDATE_COMPLETE = "update_complete" + UPDATE_FAILED = "update_failed" + ROLLBACK_IN_PROGRESS = "rollback_in_progress" + ROLLBACK_COMPLETE = "rollback_complete" + ROLLBACK_FAILED = "rollback_failed" \ No newline at end of file diff --git a/keywords/openstack/openstack/stack/object/openstack_stack_update_input.py b/keywords/openstack/openstack/stack/object/openstack_stack_update_input.py new file mode 100644 index 00000000..daaa2055 --- /dev/null +++ b/keywords/openstack/openstack/stack/object/openstack_stack_update_input.py @@ -0,0 +1,66 @@ +class OpenstackStackUpdateInput: + """ + Class to support the parameters for 'openstack stack update' command. + An example of using this command is: + + 'openstack stack update --template=file/location/stack.yaml' + + This class is able to generate this command just by previously setting the parameters. + """ + + def __init__(self): + """ + Constructor + """ + self.stack_name = None + self.template_file_name = None + self.template_file_path = None + self.return_format = 'json' + + def set_stack_name(self, stack_name: str): + """ + Setter for the 'stack_name' parameter. + """ + self.stack_name = stack_name + + def get_stack_name(self) -> str: + """ + Getter for this 'stack_name' parameter. + """ + return self.stack_name + + def set_template_file_name(self, template_file_name: str): + """ + Setter for the 'template_file_name' parameter. + """ + self.template_file_name = template_file_name + + def get_template_file_name(self) -> str: + """ + Getter for this 'template_file_name' parameter. + """ + return self.template_file_name + + def set_template_file_path(self, template_file_path: str): + """ + Setter for the 'template_file_path' parameter. + """ + self.template_file_path = template_file_path + + def get_template_file_path(self) -> str: + """ + Getter for this 'template_file_path' parameter. + """ + return self.template_file_path + + def set_return_format(self, return_format: str) -> str: + """ + Getter for this 'get_return_format' parameter. + """ + self.return_format = return_format + + def get_return_format(self) -> str: + """ + Getter for this 'return_format' parameter. + """ + return self.return_format diff --git a/keywords/openstack/openstack/stack/openstack_manage_stack_keywords.py b/keywords/openstack/openstack/stack/openstack_manage_stack_keywords.py new file mode 100644 index 00000000..f52022ad --- /dev/null +++ b/keywords/openstack/openstack/stack/openstack_manage_stack_keywords.py @@ -0,0 +1,88 @@ +from framework.resources.resource_finder import get_stx_resource_path +from framework.logging.automation_logger import get_logger +from keywords.openstack.openstack.openstack_replacement_dict_parser import OpenstackReplacementDictParser +from keywords.files.yaml_keywords import YamlKeywords +from keywords.openstack.openstack.stack.object.openstack_manage_stack_create_input import OpenstackManageStackCreateInput +from keywords.openstack.openstack.stack.object.openstack_manage_stack_delete_input import \ + OpenstackManageStackDeleteInput +from keywords.openstack.openstack.stack.object.openstack_stack_delete_input import OpenstackStackDeleteInput +from keywords.openstack.openstack.stack.openstack_stack_create_keywords import OpenstackStackCreateKeywords +from keywords.openstack.openstack.stack.object.openstack_stack_create_input import OpenstackStackCreateInput +from keywords.openstack.openstack.stack.openstack_stack_delete_keywords import OpenstackStackDeleteKeywords +from keywords.openstack.openstack.stack.openstack_stack_list_keywords import OpenstackStackListKeywords + + +class OpenstackManageStack: + """ + Class for Managing stacks support + """ + + def __init__(self, ssh_connection): + self.ssh_connection = ssh_connection + + def create_stacks(self, openstack_manage_stack_create_input: OpenstackManageStackCreateInput): + """ + Executes a sequence of stack_create commands based on a OpenstackManageStackCreateInput given + Validates if given stack is already created before creating it again + + Args: OpenstackManageStackCreateInput + + Returns: None + """ + + resource_list = openstack_manage_stack_create_input.get_resource_list() + file_destination_location = openstack_manage_stack_create_input.get_file_destination() + + if resource_list is None or file_destination_location is None: + error_message = "resource_list and file_destination_location are required" + get_logger().log_exception(error_message) + raise ValueError(error_message) + + for key, values in resource_list.items(): + for value in values: + stack_name = f"{key}_{value['name']}" + if OpenstackStackCreateKeywords.is_already_created(stack_name): + continue + output_file_name = f"{stack_name}.yaml" + template_file = get_stx_resource_path(f"resources/openstack/stack/template/{key}.yaml") + replacement_dictionary = OpenstackReplacementDictParser(value, key).get_replacement_dict() + YamlKeywords(self.ssh_connection).generate_yaml_file_from_template( + template_file, replacement_dictionary, + output_file_name, + file_destination_location + ) + openstack_stack_create = OpenstackStackCreateInput() + openstack_stack_create.set_stack_name(stack_name) + openstack_stack_create.set_template_file_name(output_file_name) + + OpenstackStackCreateKeywords(self.ssh_connection).openstack_stack_create(openstack_stack_create) + + def delete_stacks(self, openstack_manage_stack_delete_input: OpenstackManageStackDeleteInput): + """ + Executes a sequence of stack_delete commands based on a OpenstackManageStackDeleteInput given + Will not delete stacks listed in the skip_list of that ManageStackDeleteInput + Gets all existing stacks and if a stack with the given file exists, it will delete it + + Args: OpenstackManageStackDeleteInput + + Returns: None + """ + + resource_list = openstack_manage_stack_delete_input.get_resource_list() + skip_list = openstack_manage_stack_delete_input.get_skip_list() + + if resource_list is None: + error_message = "resource_list is required" + get_logger().log_exception(error_message) + raise ValueError(error_message) + + openstack_stack_list = OpenstackStackListKeywords(self.ssh_connection).get_openstack_stack_list().get_stacks() + for key, values in resource_list.items(): + if key in skip_list: + continue + for value in values: + stack_name = f"{key}_{value['name']}" + if stack_name in openstack_stack_list: + openstack_stack_delete_input = OpenstackStackDeleteInput() + openstack_stack_delete_input.set_stack_name(stack_name) + OpenstackStackDeleteKeywords(self.ssh_connection).get_openstack_stack_delete(openstack_stack_delete_input) \ No newline at end of file diff --git a/keywords/openstack/openstack/stack/openstack_stack_create_keywords.py b/keywords/openstack/openstack/stack/openstack_stack_create_keywords.py new file mode 100644 index 00000000..2ddff121 --- /dev/null +++ b/keywords/openstack/openstack/stack/openstack_stack_create_keywords.py @@ -0,0 +1,117 @@ +from framework.logging.automation_logger import get_logger +from keywords.base_keyword import BaseKeyword +from keywords.openstack.command_wrappers import source_admin_openrc +from keywords.openstack.openstack.stack.object.openstack_stack_create_input import OpenstackStackCreateInput +from keywords.openstack.openstack.stack.openstack_stack_list_keywords import OpenstackStackListKeywords +from keywords.openstack.openstack.stack.object.openstack_stack_output import OpenstackStackOutput +from keywords.openstack.openstack.stack.object.openstack_stack_status_enum import OpenstackStackStatusEnum +from keywords.python.string import String + + +class OpenstackStackCreateKeywords(BaseKeyword): + """ + Class for Openstack Stack Create + """ + + def __init__(self, ssh_connection): + """ + Constructor + Args: + ssh_connection: + """ + self.ssh_connection = ssh_connection + + def openstack_stack_create(self, openstack_stack_create_input: OpenstackStackCreateInput) -> OpenstackStackOutput: + """ + Openstack stack create function, creates a given stack + Args: + openstack_stack_create_input (OpenstackStackCreateInput): an object with the required parameters + + Returns: + openstack_stack_output: An object with the openstack stack create output + + """ + # Gets the command 'openstack stack create' with its parameters configured. + cmd = self.get_command(openstack_stack_create_input) + stack_name = openstack_stack_create_input.get_stack_name() + + # Executes the command 'openstack stack create'. + output = self.ssh_connection.send(source_admin_openrc(cmd),get_pty=True) + self.validate_success_return_code(self.ssh_connection) + openstack_stack_output = OpenstackStackOutput(output) + + # Tracks the execution of the command 'openstack stack create' until its completion or a timeout. + openstack_stack_list_keywords = OpenstackStackListKeywords(self.ssh_connection) + openstack_stack_list_keywords.validate_stack_status(stack_name, 'CREATE_COMPLETE') + + # If the execution arrived here the status of the stack is 'created'. + openstack_stack_output.get_openstack_stack_object().set_stack_status('create_complete') + + return openstack_stack_output + + def is_already_created(self, stack_name: str) -> bool: + """ + Verifies if the stack has already been created. + Args: + stack_name (str): a string representing the name of the stack. + + Returns: + bool: True if the stack named 'stack_name' has already been created; False otherwise. + + """ + try: + openstack_stack_list_keywords = OpenstackStackListKeywords(self.ssh_connection) + if openstack_stack_list_keywords.get_openstack_stack_list().is_in_stack_list(stack_name): + stack = OpenstackStackListKeywords(self.ssh_connection).get_openstack_stack_list().get_stack(stack_name) + return ( + stack.get_stack_status() == OpenstackStackStatusEnum.CREATE_IN_PROGRESS.value + or stack.get_stack_status() == OpenstackStackStatusEnum.CREATE_COMPLETE.value + or stack.get_stack_status() == OpenstackStackStatusEnum.UPDATE_IN_PROGRESS.value + or stack.get_stack_status() == OpenstackStackStatusEnum.UPDATE_COMPLETE.value + or stack.get_stack_status() == OpenstackStackStatusEnum.ROLLBACK_IN_PROGRESS.value + or stack.get_stack_status() == OpenstackStackStatusEnum.ROLLBACK_COMPLETE.value + ) + return False + except Exception as ex: + get_logger().log_exception(f"An error occurred while verifying whether the application named {stack_name} is already created.") + raise ex + + def get_command(self, openstack_stack_create_input: OpenstackStackCreateInput) -> str: + """ + Generates a string representing the 'openstack stack create' command with parameters based on the values in + the 'openstack_stack_create_input' argument. + Args: + openstack_stack_create_input (OpenstackStackCreateInput): an instance of OpenstackStackCreateInput + configured with the parameters needed to execute the 'openstack stack create' command properly. + + Returns: + str: a string representing the 'openstack stack create' command, configured according to the parameters + in the 'openstack_stack_create_input' argument. + + """ + + # 'template_file_path' and 'template_file_name' properties are required + template_file_path = openstack_stack_create_input.get_template_file_path() + template_file_name = openstack_stack_create_input.get_template_file_name() + if String.is_empty(template_file_path) or String.is_empty(template_file_name): + error_message = "Template path and name must be specified" + get_logger().log_exception(error_message) + raise ValueError(error_message) + template_file_path_as_param = ( + f'--template={openstack_stack_create_input.get_template_file_path()}/{openstack_stack_create_input.get_template_file_name()}' + ) + + # 'stack_name' is required + stack_name = openstack_stack_create_input.get_stack_name() + if String.is_empty(stack_name): + error_message = "Stack name is required" + get_logger().log_exception(error_message) + raise ValueError(error_message) + + # 'return_format' property is optional. + return_format_as_param = f'-f {openstack_stack_create_input.get_return_format()}' + + # Assembles the command. + cmd = f'openstack stack create {return_format_as_param} {template_file_path_as_param} {stack_name}' + + return cmd diff --git a/keywords/openstack/openstack/stack/openstack_stack_delete_keywords.py b/keywords/openstack/openstack/stack/openstack_stack_delete_keywords.py new file mode 100644 index 00000000..ed5c248b --- /dev/null +++ b/keywords/openstack/openstack/stack/openstack_stack_delete_keywords.py @@ -0,0 +1,50 @@ +from keywords.base_keyword import BaseKeyword +from keywords.openstack.command_wrappers import source_admin_openrc +from keywords.openstack.openstack.stack.object.openstack_stack_delete_input import OpenstackStackDeleteInput + + +class OpenstackStackDeleteKeywords(BaseKeyword): + """ + Class for Openstack stack delete keywords + """ + + def __init__(self, ssh_connection): + """ + Constructor + Args: + ssh_connection: + """ + self.ssh_connection = ssh_connection + + def get_openstack_stack_delete(self, openstack_stack_delete_input: OpenstackStackDeleteInput) -> str: + """ + Delete a stack specified in the parameter 'openstack_stack_delete_input'. + Args: + openstack_stack_delete_input (OpenstackStackDeleteInput): defines the stack name + + Returns: + str: a string message indicating the result of the deletion. Examples: + 'Stack hello-kitty deleted.\n' + 'Stack-delete rejected: stack not found.\n' + """ + cmd = self.get_command(openstack_stack_delete_input) + output = self.ssh_connection.send(source_admin_openrc(cmd),get_pty=True) + self.validate_success_return_code(self.ssh_connection) + + return output[0] + + def get_command(self, openstack_stack_delete_input: OpenstackStackDeleteInput) -> str: + """ + Generates a string representing the 'openstack stack delete' command with parameters based on the values in + the 'openstack_stack_delete_input' argument. + Args: + openstack_stack_delete_input (OpenstackStackDeleteInput): an instance of OpenstackStackDeleteInput + configured with the parameters needed to execute the 'openstack stack delete' command properly. + + Returns: + str: a string representing the 'openstack stack delete' command, configured according to the parameters + in the 'openstack_stack_delete_input' argument. + + """ + cmd = f'openstack stack delete -y {openstack_stack_delete_input.get_stack_name()}' + return cmd diff --git a/keywords/openstack/openstack/stack/openstack_stack_list_keywords.py b/keywords/openstack/openstack/stack/openstack_stack_list_keywords.py new file mode 100644 index 00000000..101388ca --- /dev/null +++ b/keywords/openstack/openstack/stack/openstack_stack_list_keywords.py @@ -0,0 +1,55 @@ +from framework.logging.automation_logger import get_logger +from framework.validation.validation import validate_equals_with_retry +from keywords.base_keyword import BaseKeyword +from keywords.openstack.command_wrappers import source_admin_openrc +from keywords.openstack.openstack.stack.object.openstack_stack_list_output import OpenstackStackListOutput + + +class OpenstackStackListKeywords(BaseKeyword): + """ + Class for Openstack stack list keywords. + """ + + def __init__(self, ssh_connection): + """ + Constructor + Args: + ssh_connection: + """ + self.ssh_connection = ssh_connection + + def get_openstack_stack_list(self) -> OpenstackStackListOutput: + """ + Gets a OpenstackStackListOutput object related to the execution of the 'openstack stack list' command. + + Args: None + + Returns: + OpenstackStackListOutput: an instance of the OpenstackStackListOutput object representing the + heat stacks on the host, as a result of the execution of the 'openstack stack list' command. + """ + output = self.ssh_connection.send(source_admin_openrc('openstack stack list -f json'),get_pty=True) + self.validate_success_return_code(self.ssh_connection) + openstack_stack_list_output = OpenstackStackListOutput(output) + + return openstack_stack_list_output + + def validate_stack_status(self, stack_name: str, status: str): + """ + This function will validate that the stack specified reaches the desired status. + Args: + stack_name: Name of the stack that we are waiting for. + status: Status in which we want to wait for the stack to reach. + + Returns: None + + """ + + def get_stack_status(): + openstack_stacks = self.get_openstack_stack_list() + stack_status = openstack_stacks.get_stack(stack_name).get_stack_status() + return stack_status + + message = f"Openstack stack {stack_name}'s status is {status}" + validate_equals_with_retry(get_stack_status, status, message, timeout=300) + diff --git a/keywords/openstack/openstack/stack/openstack_stack_update_keywords.py b/keywords/openstack/openstack/stack/openstack_stack_update_keywords.py new file mode 100644 index 00000000..cc7a961f --- /dev/null +++ b/keywords/openstack/openstack/stack/openstack_stack_update_keywords.py @@ -0,0 +1,109 @@ +from framework.logging.automation_logger import get_logger +from keywords.base_keyword import BaseKeyword +from keywords.openstack.command_wrappers import source_admin_openrc +from keywords.openstack.openstack.stack.object.openstack_stack_update_input import OpenstackStackUpdateInput +from keywords.openstack.openstack.stack.openstack_stack_list_keywords import OpenstackStackListKeywords +from keywords.openstack.openstack.stack.object.openstack_stack_output import OpenstackStackOutput +from keywords.openstack.openstack.stack.object.openstack_stack_status_enum import OpenstackStackStatusEnum +from keywords.python.string import String + + +class OpenstackStackUpdateKeywords(BaseKeyword): + """ + Class for Openstack Stack Update + """ + + def __init__(self, ssh_connection): + """ + Constructor + Args: + ssh_connection: + """ + self.ssh_connection = ssh_connection + + def openstack_stack_update(self, openstack_stack_update_input: OpenstackStackUpdateInput) -> OpenstackStackOutput: + """ + Openstack stack update function, updates a given existing stack + Args: + openstack_stack_update_input (OpenstackStackUpdateInput): an object with the required parameters + + Returns: + openstack_stack_output: An object with the openstack stack update output + + """ + # Gets the command 'openstack stack update' with its parameters configured. + cmd = self.get_command(openstack_stack_update_input) + stack_name = openstack_stack_update_input.get_stack_name() + + # Executes the command 'openstack stack update'. + output = self.ssh_connection.send(source_admin_openrc(cmd), get_pty=True) + self.validate_success_return_code(self.ssh_connection) + openstack_stack_output = OpenstackStackOutput(output) + + # Tracks the execution of the command 'openstack stack update' until its completion or a timeout. + openstack_stack_list_keywords = OpenstackStackListKeywords(self.ssh_connection) + openstack_stack_list_keywords.validate_stack_status(stack_name, 'UPDATE_COMPLETE') + + # If the execution arrived here the status of the stack is 'updated'. + openstack_stack_output.get_openstack_stack_object().set_stack_status('update_complete') + + return openstack_stack_output + + def is_already_updated(self, stack_name: str) -> bool: + """ + Verifies if the stack has already been updated. + Args: + stack_name (str): a string representing the name of the stack. + + Returns: + bool: True if the stack named 'stack_name' has already been updated; False otherwise. + + """ + openstack_stack_list_keywords = OpenstackStackListKeywords(self.ssh_connection) + if openstack_stack_list_keywords.get_openstack_stack_list().is_in_stack_list(stack_name): + stack = OpenstackStackListKeywords(self.ssh_connection).get_openstack_stack_list().get_stack(stack_name) + return ( + stack.get_stack_status() == OpenstackStackStatusEnum.UPDATE_IN_PROGRESS.value + or stack.get_stack_status() == OpenstackStackStatusEnum.UPDATE_COMPLETE.value + ) + return False + + def get_command(self, openstack_stack_update_input: OpenstackStackUpdateInput) -> str: + """ + Generates a string representing the 'openstack stack update' command with parameters based on the values in + the 'openstack_stack_update_input' argument. + Args: + openstack_stack_update_input (OpenstackStackUpdateInput): an instance of OpenstackStackUpdateInput + configured with the parameters needed to execute the 'openstack stack update' command properly. + + Returns: + str: a string representing the 'openstack stack update' command, configured according to the parameters + in the 'openstack_stack_update_input' argument. + + """ + + # 'template_file_path' and 'template_file_name' properties are required + template_file_path = openstack_stack_update_input.get_template_file_path() + template_file_name = openstack_stack_update_input.get_template_file_name() + if String.is_empty(template_file_path) or String.is_empty(template_file_name): + error_message = "Template path and name must be specified" + get_logger().log_exception(error_message) + raise ValueError(error_message) + template_file_path_as_param = ( + f'--template={openstack_stack_update_input.get_template_file_path()}/{openstack_stack_update_input.get_template_file_name()}' + ) + + # 'stack_name' is required + stack_name = openstack_stack_update_input.get_stack_name() + if String.is_empty(stack_name): + error_message = "Stack name is required" + get_logger().log_exception(error_message) + raise ValueError(error_message) + + # 'return_format' property is optional. + return_format_as_param = f'-f {openstack_stack_update_input.get_return_format()}' + + # Assembles the command. + cmd = f'openstack stack update {return_format_as_param} {template_file_path_as_param} {stack_name}' + + return cmd diff --git a/resources/openstack/stack/regression/basic_openstack_project_config.yaml b/resources/openstack/stack/regression/basic_openstack_project_config.yaml new file mode 100644 index 00000000..9a3cebe5 --- /dev/null +++ b/resources/openstack/stack/regression/basic_openstack_project_config.yaml @@ -0,0 +1,56 @@ +project: + - name: ace_project_1 + description: Ace 1 Project + quota: + network: 8 + subnet: 18 + port: 83 + floatingip: 10 + instances: 10 + cores: 30 + volumes: 12 + snapshots: 12 + - name: ace_project_2 + description: Ace 2 Project + quota: + network: 8 + subnet: 18 + port: 83 + floatingip: 10 + instances: 10 + cores: 30 + volumes: 12 + snapshots: 12 +user: + - name: ace_one + password: password + email: tenant1@noreply.com + default_project: ace_project_1 + - name: ace_two + password: password + email: tenant2@noreply.com + default_project: ace_project_2 +role_assignment: + - role: admin + name: ace_one_admin_ace_project_1 + project: ace_test_1 + user: ace_one + - role: admin + name: ace_two_admin_ace_project_2 + project: ace_test_2 + user: ace_two +webimage: + - name: ace_cirros_image + container_format: bare + disk_format: raw + location: http://download.cirros-cloud.net/0.4.0/cirros-0.4.0-x86_64-disk.img + min_disk: 0 + min_ram: 0 + visibility: public +flavor: + - name: ace_small + ram: 1024 + disk: 2 + vcpus: 1 + extra_specs: + hw:mem_page_size: large diff --git a/resources/openstack/stack/template/flavor.yaml b/resources/openstack/stack/template/flavor.yaml new file mode 100644 index 00000000..9794d198 --- /dev/null +++ b/resources/openstack/stack/template/flavor.yaml @@ -0,0 +1,45 @@ +heat_template_version: wallaby + +description: > + Heat template to create OpenStack flavors. + +parameters: + name: + type: string + default: "{{ name }}" + description: Name of the flavor + + ram: + type: number + default: "{{ ram }}" + description: Amount of RAM in MB + + vcpus: + type: number + default: "{{ vcpus }}" + description: Number of VCPUs + + disk: + type: number + default: "{{ disk }}" + description: Size of disk in GB + + extra_specs: + type: json + default: {{ extra_specs }} + description: Properties of the flavor + +resources: + flavor: + type: OS::Nova::Flavor + properties: + name: { get_param: name } + ram: { get_param: ram } + vcpus: { get_param: vcpus } + disk: { get_param: disk } + extra_specs: { get_param: extra_specs } + +outputs: + flavor_id: + description: ID of the created flavor + value: { get_resource: flavor } diff --git a/resources/openstack/stack/template/network.yaml b/resources/openstack/stack/template/network.yaml new file mode 100644 index 00000000..f2d6bdb5 --- /dev/null +++ b/resources/openstack/stack/template/network.yaml @@ -0,0 +1,119 @@ +heat_template_version: wallaby + +description: > + Heat template to create OpenStack networks and subnets with project name and QoS policy name lookup. + +parameters: + network_name: + type: string + default: "{{ name }}" + description: Name of the network + + subnet_name: + type: string + default: "{{ subnet_name }}" + description: Name of the subnet + + project_name: + type: string + default: "{{ project_name }}" + description: Project name associated with the network + + qos_policy_name: + type: string + default: "{{ qos_policy_name }}" + description: QoS policy name associated with the network + + provider_network_type: + type: string + default: "{{ provider_network_type }}" + description: Provider network type (e.g., vlan) + + provider_physical_network: + type: string + default: "{{ provider_physical_network }}" + description: Provider physical network name + + shared: + type: boolean + default: "{{ shared }}" + description: Whether the network is shared + + external: + type: boolean + default: "{{ external }}" + description: Whether the network is external + + gateway_ip: + type: string + default: "{{ gateway_ip }}" + description: Gateway IP for the subnet + + enable_dhcp: + type: boolean + default: "{{ enable_dhcp }}" + description: Whether DHCP is disabled for the subnet + + subnet_range: + type: string + default: "{{ subnet_range }}" + description: Subnet range in CIDR format + + ip_version: + type: number + default: "{{ ip_version }}" + description: IP version (e.g., 4) + + allocation_pool_start: + type: string + default: "{{ allocation_pool_start }}" + description: Start of the allocation pool + + allocation_pool_end: + type: string + default: "{{ allocation_pool_end }}" + description: End of the allocation pool + +resources: + project: + type: OS::Keystone::Project + properties: + name: { get_param: project_name } + + qos_policy: + type: OS::Neutron::QoSPolicy + properties: + name: { get_param: qos_policy_name } + + network: + type: OS::Neutron::Net + properties: + name: { get_param: network_name } + tenant_id: { get_resource: project } + provider:network_type: { get_param: provider_network_type } + provider:physical_network: { get_param: provider_physical_network } + shared: { get_param: shared } + external: { get_param: external } + qos_policy: { get_resource: qos_policy } + + subnet: + type: OS::Neutron::Subnet + properties: + name: { get_param: subnet_name } + network_id: { get_resource: network } + gateway_ip: { get_param: gateway_ip } + enable_dhcp: { get_param: enable_dhcp } + cidr: { get_param: subnet_range } + ip_version: { get_param: ip_version } + allocation_pools: + - start: { get_param: allocation_pool_start } + end: { get_param: allocation_pool_end } + +outputs: + network_id: + description: ID of the created network + value: { get_resource: network } + + subnet_id: + description: ID of the created subnet + value: { get_resource: subnet } diff --git a/resources/openstack/stack/template/network_segment.yaml b/resources/openstack/stack/template/network_segment.yaml new file mode 100644 index 00000000..518e88f9 --- /dev/null +++ b/resources/openstack/stack/template/network_segment.yaml @@ -0,0 +1,62 @@ +heat_template_version: wallaby + +description: > + Heat template to create OpenStack network segment ranges with project name lookup. + +parameters: + name: + type: string + default: "{{ name }}" + description: Name of the network segment range + + network_type: + type: string + default: "{{ network_type }}" + description: Network type (e.g., vlan) + + physical_network: + type: string + default: "{{ physical_network }}" + description: Physical network name + + minimum: + type: number + default: "{{ minimum_range }}" + description: Minimum VLAN ID + + maximum: + type: number + default: "{{ maximum_range }}" + description: Maximum VLAN ID + + shared: + type: boolean + default: "{{ shared }}" + description: Whether the segment range is shared + + project_name: + type: string + default: "{{ project_name }}" + description: Project name associated with the segment range (optional) + +resources: + project: + type: OS::Keystone::Project + properties: + name: { get_param: project_name } + + network_segment: + type: OS::Neutron::Segment + properties: + name: { get_param: name } + network_type: { get_param: network_type } + physical_network: { get_param: physical_network } + minimum: { get_param: minimum_range } + maximum: { get_param: maximum_range } + shared: { get_param: shared } + project_id: { get_resource: project } + +outputs: + segment_range_id: + description: ID of the created network segment range + value: { get_resource: network_segment } diff --git a/resources/openstack/stack/template/project.yaml b/resources/openstack/stack/template/project.yaml new file mode 100644 index 00000000..d974286d --- /dev/null +++ b/resources/openstack/stack/template/project.yaml @@ -0,0 +1,90 @@ +heat_template_version: wallaby + +description: > + Heat template to create a single OpenStack project with quotas. + +parameters: + name: + type: string + default: "{{ name }}" + description: Name of the project + + description: + type: string + default: "{{ description }}" + description: Description of the project + + network: + type: number + default: "{{ network }}" + description: Quota for the number of networks + + subnet: + type: number + default: "{{ subnet }}" + description: Quota for the number of subnets + + port: + type: number + default: "{{ port }}" + description: Quota for the number of ports + + floatingip: + type: number + default: "{{ floatingip }}" + description: Quota for the number of floating IPs + + instances: + type: number + default: "{{ instances }}" + description: Quota for the number of instances + + cores: + type: number + default: "{{ cores }}" + description: Quota for the number of cores + + volumes: + type: number + default: "{{ volumes }}" + description: Quota for the number of volumes + + snapshots: + type: number + default: "{{ snapshots }}" + description: Quota for the number of snapshots + +resources: + project: + type: OS::Keystone::Project + properties: + name: { get_param: name } + description: { get_param: description } + + cinder_quota: + type: OS::Cinder::Quota + properties: + project: { get_resource: project } + volumes: { get_param: volumes } + snapshots: { get_param: snapshots } + + neutron_quota: + type: OS::Neutron::Quota + properties: + project: { get_resource: project } + network: { get_param: network } + subnet: { get_param: subnet } + port: { get_param: port } + floatingip: { get_param: floatingip } + + nova_quota: + type: OS::Nova::Quota + properties: + project: { get_resource: project } + cores: { get_param: cores } + instances: { get_param: instances } + +outputs: + project_id: + description: ID of the created project + value: { get_attr: [project, show, id] } diff --git a/resources/openstack/stack/template/qos.yaml b/resources/openstack/stack/template/qos.yaml new file mode 100644 index 00000000..99da2d52 --- /dev/null +++ b/resources/openstack/stack/template/qos.yaml @@ -0,0 +1,61 @@ +heat_template_version: wallaby + +description: > + Heat template to create OpenStack QoS policies. + +parameters: + name: + type: string + default: "{{ name }}" + description: Name of the QoS policy + + description: + type: string + default: "{{ description }}" + description: Description of the QoS policy + + project_name: + type: string + default: "{{ project_name }}" + description: Project ID associated with the QoS policy + + max_kbps: + type: number + default: "{{ max_kbps }}" + description: Maximum bandwidth in kbps + + max_burst_kbps: + type: number + default: "{{ max_burst_kbps }}" + description: Maximum burst bandwidth in kbps + + dscp_mark: + type: number + default: "{{ dscp_mark }}" + description: DSCP marking value + +resources: + project: + type: OS::Keystone::Project + properties: + name: { get_param: project_name } + + qos_policy: + type: OS::Neutron::QoSPolicy + properties: + name: { get_param: name } + description: { get_param: description } + tenant_id: { get_resource: project } + rules: + - type: OS::Neutron::QoSBandwidthLimitRule + properties: + max_kbps: { get_param: max_kbps } + max_burst_kbps: { get_param: max_burst_kbps } + - type: OS::Neutron::QoSDscpMarkingRule + properties: + dscp_mark: { get_param: dscp_mark } + +outputs: + qos_policy_id: + description: ID of the created QoS policy + value: { get_resource: qos_policy } diff --git a/resources/openstack/stack/template/role_assignment.yaml b/resources/openstack/stack/template/role_assignment.yaml new file mode 100644 index 00000000..6bb7de97 --- /dev/null +++ b/resources/openstack/stack/template/role_assignment.yaml @@ -0,0 +1,34 @@ +heat_template_version: wallaby + +description: > + Heat template to assign roles to users in a project. + +parameters: + project: + type: string + default: "{{ project }}" + description: Name of the project + + user: + type: string + default: "{{ user }}" + description: Name of the user + + role: + type: string + default: "{{ role }}" + description: Name of the role to assign + +resources: + role_assignment: + type: OS::Keystone::UserRoleAssignment + properties: + user: { get_param: user } + roles: + - project: { get_param: project } + role: { get_param: role } + +outputs: + role_assignment_id: + description: ID of the role assignment + value: { get_resource: role_assignment } \ No newline at end of file diff --git a/resources/openstack/stack/template/user.yaml b/resources/openstack/stack/template/user.yaml new file mode 100644 index 00000000..e3e28954 --- /dev/null +++ b/resources/openstack/stack/template/user.yaml @@ -0,0 +1,39 @@ +heat_template_version: wallaby + +description: > + Heat template to create OpenStack users. + +parameters: + name: + type: string + default: "{{ name }}" + description: Name of the user + + password: + type: string + default: "{{ password }}" + description: Password for the user + + email: + type: string + default: "{{ email }}" + description: Email of the user + + default_project: + type: string + default: "{{ default_project }}" + description: Name of the project to assign the user to + +resources: + user: + type: OS::Keystone::User + properties: + name: { get_param: name } + password: { get_param: password } + email: { get_param: email } + default_project: { get_param: default_project } + +outputs: + user_id: + description: ID of the created user + value: { get_resource: user } \ No newline at end of file diff --git a/resources/openstack/stack/template/webimage.yaml b/resources/openstack/stack/template/webimage.yaml new file mode 100644 index 00000000..1e365df4 --- /dev/null +++ b/resources/openstack/stack/template/webimage.yaml @@ -0,0 +1,57 @@ +heat_template_version: wallaby + +description: > + Heat template to create a Glance image with optional minimum disk and minimum RAM properties. + +parameters: + name: + type: string + default: "{{ name }}" + description: Name of the image + + container_format: + type: string + default: "{{ container_format }}" + description: Container format of the image + + disk_format: + type: string + default: "{{ disk_format }}" + description: Disk format of the image + + location: + type: string + default: "{{ location }}" + description: Path to the image file + + min_disk: + type: number + description: Minimum disk size (optional) + default: "{{ min_disk }}" + + min_ram: + type: number + description: Minimum RAM size (optional) + default: "{{ min_ram }}" + + visibility: + type: string + description: Is the image public + default: "{{ visibility }}" + +resources: + glance_image: + type: OS::Glance::WebImage + properties: + name: { get_param: name } + container_format: { get_param: container_format } + disk_format: { get_param: disk_format } + visibility: { get_param: visibility } + location: { get_param: location } + min_disk: { get_param: min_disk } + min_ram: { get_param: min_ram } + +outputs: + image_id: + description: ID of the created image + value: { get_attr: [glance_image, show, id] } diff --git a/testcases/openstack/openstack_stack.py b/testcases/openstack/openstack_stack.py new file mode 100644 index 00000000..5ba6a1b0 --- /dev/null +++ b/testcases/openstack/openstack_stack.py @@ -0,0 +1,52 @@ +from pytest import mark +from framework.resources.resource_finder import get_stx_resource_path +from keywords.cloud_platform.ssh.lab_connection_keywords import LabConnectionKeywords +from keywords.files.file_keywords import FileKeywords +from keywords.files.yaml_keywords import YamlKeywords +from keywords.openstack.openstack.stack.object.openstack_manage_stack_delete_input import \ + OpenstackManageStackDeleteInput +from keywords.openstack.openstack.stack.openstack_manage_stack_keywords import OpenstackManageStack +from keywords.openstack.openstack.stack.object.openstack_manage_stack_create_input import OpenstackManageStackCreateInput +from keywords.openstack.openstack.stack.object.openstack_stack_delete_input import OpenstackStackDeleteInput +from keywords.openstack.openstack.stack.openstack_stack_delete_keywords import OpenstackStackDeleteKeywords +from keywords.openstack.openstack.stack.openstack_stack_list_keywords import OpenstackStackListKeywords + + +@mark.p1 +def test_project_stack_create_and_delete(): + """ + Tests the build of a simple Openstack lab environment using a YAML file and the openstack stack create command + + Test Steps: + - loads a basic YAML file containing environment properties + - connect to active controller + - creates a folder to store generated heat templates + - generates/uploads a heat template file in the remote repository + - goes by each object of that YAML file and create that resource using the uploaded template + and openstack stack create command + - does not generate Neutron related resources + """ + lab_connect_keywords = LabConnectionKeywords() + ssh_connection = lab_connect_keywords.get_active_controller_ssh() + + heat_file_destination = "/var/opt/openstack/heat" + + lab_config_file_location = get_stx_resource_path("resources/openstack/stack/regression/basic_openstack_project_config.yaml") + lab_config = YamlKeywords(ssh_connection).load_yaml(lab_config_file_location) + FileKeywords(ssh_connection).create_directory(heat_file_destination) + + openstack_manage_stack_creation = OpenstackManageStackCreateInput() + openstack_manage_stack_creation.set_file_destination(heat_file_destination) + openstack_manage_stack_creation.set_resource_list(lab_config) + OpenstackManageStack(ssh_connection).create_stacks(openstack_manage_stack_creation) + + lab_connect_keywords = LabConnectionKeywords() + ssh_connection = lab_connect_keywords.get_active_controller_ssh() + + lab_config_file_location = get_stx_resource_path("resources/openstack/stack/regression/basic_openstack_project_config.yaml") + lab_config = YamlKeywords(ssh_connection).load_yaml(lab_config_file_location) + + openstack_manage_stack_deletion = OpenstackManageStackDeleteInput() + openstack_manage_stack_deletion.set_resource_list(lab_config) + + OpenstackManageStack(ssh_connection).delete_stacks(openstack_manage_stack_deletion) \ No newline at end of file