From 1e85bfc73098aa91d18f5d61811c94c6f4e00a32 Mon Sep 17 00:00:00 2001 From: rajeshP524 Date: Thu, 4 Jan 2024 11:30:24 +0530 Subject: [PATCH] Crucible network workload Added a new workload in browbeat, where in it uses Crucible to run benchmarks. This workload uses Crucible remote endpoint against Openstack VMs. Change-Id: If374ed803fe57f04acaa4c64ae037516eb6f4fa9 --- browbeat-config.yaml | 30 +++ .../rally-plugins/workloads/crucible-uperf.py | 240 ++++++++++++++++++ .../workloads/crucible-uperf.yml | 59 +++++ .../workloads/crucible/README.rst | 62 +++++ .../crucible/ansible/browbeat_bootstrap.yaml | 27 ++ .../workloads/crucible/mv_params.json | 22 ++ .../crucible/templates/uperf_script.sh.j2 | 16 ++ .../workloads/crucible/templates/vlans.sh.j2 | 54 ++++ .../workloads/provider_net_context.py | 239 +++++++++++++++++ 9 files changed, 749 insertions(+) create mode 100644 rally/rally-plugins/workloads/crucible-uperf.py create mode 100644 rally/rally-plugins/workloads/crucible-uperf.yml create mode 100644 rally/rally-plugins/workloads/crucible/README.rst create mode 100644 rally/rally-plugins/workloads/crucible/ansible/browbeat_bootstrap.yaml create mode 100644 rally/rally-plugins/workloads/crucible/mv_params.json create mode 100644 rally/rally-plugins/workloads/crucible/templates/uperf_script.sh.j2 create mode 100644 rally/rally-plugins/workloads/crucible/templates/vlans.sh.j2 create mode 100644 rally/rally-plugins/workloads/provider_net_context.py diff --git a/browbeat-config.yaml b/browbeat-config.yaml index 5d2a9afc2..f89f9fd26 100644 --- a/browbeat-config.yaml +++ b/browbeat-config.yaml @@ -909,3 +909,33 @@ workloads: sample: 1 ansible_forks: 5 file: rally/rally-plugins/pbench-fio/pbench-fio.yml + + - name: Network + enabled: false + type: rally + rally_deployment: overcloud + concurrency: + - 1 + times: 1 + scenarios: + - name: crucible-uperf + enabled: true + image_name: rhel9 + flavor_name: m1.small + num_provider_networks: 10 + cidr_prefix: "192.18" + provider_phys_net: "datacentre" + server_client_pairs_per_network: 5 + # do not append '/' at the end for dir_path + crucible_repo_dir_path: /home/stack/crucible_repos + # crucible controller info + hostname_or_ip: + user: root + password: + ext_iface: eno4 + # crucible run params + client_userenv: "stream8" + tags: "browbeat:browbeat" + profile_path: "mv_params.json" + num_samples: 3 + file: rally/rally-plugins/workloads/crucible-uperf.yml diff --git a/rally/rally-plugins/workloads/crucible-uperf.py b/rally/rally-plugins/workloads/crucible-uperf.py new file mode 100644 index 000000000..d3ff453bd --- /dev/null +++ b/rally/rally-plugins/workloads/crucible-uperf.py @@ -0,0 +1,240 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from rally_openstack.common import consts +from rally_openstack.task.scenarios.nova import utils as nova_utils +from rally_openstack.task.scenarios.neutron import utils as neutron_utils +from rally.task import scenario +from rally.task import types +from rally.task import validation +from rally.common import logging + +import concurrent.futures +import threading +import os +from jinja2 import Environment +from jinja2 import FileSystemLoader +import paramiko +import time + +LOG = logging.getLogger(__name__) + +@types.convert(image={"type": "glance_image"}, flavor={"type": "nova_flavor"}) +@validation.add("image_valid_on_flavor", flavor_param="flavor", image_param="image") +@validation.add("required_services", services=[consts.Service.NEUTRON, consts.Service.NOVA]) +@validation.add("required_platform", platform="openstack", users=True) +@validation.add("required_contexts", contexts=("crucible_provider_networks")) +@scenario.configure(context={"cleanup@openstack": ["neutron", "nova"],"keypair@openstack": {}}, + name="BrowbeatPlugin.crucible_uperf", + platform="openstack") +class CrucibleUperf(nova_utils.NovaScenario, neutron_utils.NeutronScenario): + def __init__(self, *args, **kwargs): + super(CrucibleUperf, self).__init__(*args, **kwargs) + # Create a lock for the dictionary + self.dictionary_lock = threading.Lock() + self.server_client_info_dict = {} + + def deploy_vms_per_network(self, image, flavor, provider_network, + server_client_pairs_per_network): + try: + kwargs = {} + kwargs["nics"] = [{"net-id": provider_network["id"]}] + kwargs["key_name"] = self.context["user"]["keypair"]["name"] + + for i in range(server_client_pairs_per_network): + server = self._boot_server(image, flavor, **kwargs) + client = self._boot_server(image, flavor, **kwargs) + + # Update server and client names + server_name = provider_network["name"] + '_server_' + str(i + 1) + client_name = provider_network["name"] + '_client_' + str(i + 1) + + # Acquire lock before updating the dictionary + with self.dictionary_lock: + self.server_client_info_dict[server_name] = ( + list(server.addresses.values())[0][0]['addr']) + self.server_client_info_dict[client_name] = ( + list(client.addresses.values())[0][0]['addr']) + + except Exception as e: + print(f"Error in deploy_vms_per_network: {e}") + + def generate_entry_strings(self, N, perN): + inventory_str = "[instances]\n" + name_ip_mappings_str = "" + endpoint_str = "" + for i in range(1, N + 1): + for j in range(1, perN + 1): + server_name = f"provider_{i}_server_{j}" + client_name = f"provider_{i}_client_{j}" + + inventory_str += ( + f"{self.server_client_info_dict[server_name]} " + f"ansible_user=cloud-user ansible_private_key_file=/root/browbeat_pkey\n" + ) + inventory_str += ( + f"{self.server_client_info_dict[client_name]} " + f"ansible_user=cloud-user ansible_private_key_file=/root/browbeat_pkey\n" + ) + + name_ip_mappings_str += \ + f"{server_name}=\"{self.server_client_info_dict[server_name]}\"\n" + name_ip_mappings_str += \ + f"{client_name}=\"{self.server_client_info_dict[client_name]}\"\n" + + index = (i - 1) * perN + j + endpoint_str += ( + f" --endpoint remotehost,user:root,host:${client_name}," + f"client:{index}-{index},userenv:${{client_userenv}} \\\n" + ) + endpoint_str += ( + f" --endpoint remotehost,user:root,host:${server_name}," + f"server:{index}-{index},userenv:${{client_userenv}} \\\n" + ) + + endpoint_str = endpoint_str.rstrip('\n').rstrip('\\') + return inventory_str, name_ip_mappings_str, endpoint_str + + def render_template(self, env, template, client_userenv, tags, profile, num_samples, + name_ip_mappings_str, endpoint_str, output_file_path): + rendered_template = template.render( + client_userenv=client_userenv, + tags=tags, + profile=profile, + num_samples=num_samples, + name_ip_mappings_str=name_ip_mappings_str, + endpoint_str=endpoint_str + ) + + with open(output_file_path, 'w') as file: + file.write(rendered_template) + + def run_bootstrap(self, host, username, password): + try: + with paramiko.SSHClient() as ssh: + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh.connect(host, username=username, password=password) + # set permissions on pkey before ansible uses that file + ssh.exec_command("/usr/bin/chmod 0600 /root/browbeat_pkey") + cmd = "/usr/bin/ansible-playbook -i browbeat_ansible_inventory.ini -f 16 " \ + "-vvv /root/browbeat_bootstrap.yaml &> /root/browbeat_bootstrap.log" + _, stdout, stderr = ssh.exec_command(cmd) + + # Check the exit status of the command + if stdout.channel.recv_exit_status() == 0: + LOG.info(f"Bootstrap playbook on {host} executed successfully.") + return True + else: + raise Exception( + f"Ansible execution failed on {host}. Error: {stderr.read().decode()}" + ) + except Exception as e: + raise Exception(f"Error in run_bootstrap: {e}") + + def run_crucible_uperf(self, host, username, password): + try: + with paramiko.SSHClient() as ssh: + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh.connect(host, username=username, password=password) + ssh.exec_command("/usr/bin/chmod 777 /root/browbeat_uperf_script.sh") + cmd = "/root/browbeat_uperf_script.sh &> /root/browbeat_uperf_script.log" + _, stdout, stderr = ssh.exec_command(cmd) + + # Check the exit status of the command + if stdout.channel.recv_exit_status() == 0: + LOG.info(f"Crucible uperf script on {host} executed successfully.") + return True + else: + raise Exception( + f"Crucible uperf execution failed on {host}." + f" Error: {stderr.read().decode()}" + ) + except Exception as e: + raise Exception(f"Error in run_crucible_uperf: {e}") + + def copy_crucible_results(self, hostname_or_ip, user, password, crucible_result_dir): + cmd = ( + f"sshpass -p '{password}' scp -r {user}@{hostname_or_ip}:/var/lib/crucible/run/latest/" + f" {crucible_result_dir}/" + ) + os.system(cmd) + + def run(self, image, flavor, server_client_pairs_per_network, hostname_or_ip, user, password, + num_provider_networks, client_userenv, tags, profile, num_samples, + crucible_repo_dir_path, **kwargs): + provider_networks = self.context["provider_networks"] + with concurrent.futures.ThreadPoolExecutor(max_workers=16) as executor: + network_futures = [ + executor.submit( + self.deploy_vms_per_network, image, flavor, provider_network, + server_client_pairs_per_network + ) + for provider_network in provider_networks + ] + concurrent.futures.wait(network_futures) + + for instance_name, ip_address in self.server_client_info_dict.items(): + print(f"{instance_name}: {ip_address}") + + pkey = self.context["user"]["keypair"]["private"] + with open("./rally/rally-plugins/workloads/crucible/browbeat_pkey", 'w') as file: + file.write(pkey) + + # prepare ansible inventory and uperf script entry strings + inventory_str, name_ip_mappings_str, endpoint_str = \ + self.generate_entry_strings(num_provider_networks, server_client_pairs_per_network) + with open( + "./rally/rally-plugins/workloads/crucible/browbeat_ansible_inventory.ini", 'w' + ) as file: + file.write(inventory_str) + + # prepare the script + env = Environment(loader=FileSystemLoader(os.getcwd())) + template_path = './rally/rally-plugins/workloads/crucible/templates/uperf_script.sh.j2' + template = env.get_template(template_path) + self.output_file_path = './rally/rally-plugins/workloads/crucible/browbeat_uperf_script.sh' + self.render_template(env, template, client_userenv, tags, profile, num_samples, + name_ip_mappings_str, endpoint_str, self.output_file_path) + + # copy over scp + scp_commands = [ + f"sshpass -p '{password}' scp ./rally/rally-plugins/workloads/crucible/browbeat* " + f"{user}@{hostname_or_ip}:/root/", + f"sshpass -p '{password}' scp ./rally/rally-plugins/workloads/crucible/mv_params.json " + f"{user}@{hostname_or_ip}:/root/", + f"sshpass -p '{password}' scp ./rally/rally-plugins/workloads/crucible/ansible/" + f"browbeat* {user}@{hostname_or_ip}:/root/" + ] + + # Execute scp commands + for scp_command in scp_commands: + os.system(scp_command) + + # copy repos to crucible controller over ssh + repo_names = os.listdir(crucible_repo_dir_path) + for repo_name in repo_names: + repo = crucible_repo_dir_path + "/" + repo_name + scp_command = ( + f"sshpass -p '{password}' scp {repo} {user}@{hostname_or_ip}:/root/" + ) + os.system(scp_command) + + time.sleep(60) + self.run_bootstrap(hostname_or_ip, user, password) + self.run_crucible_uperf(hostname_or_ip, user, password) + + # copy results + with open('../rally_result_dir_path') as f: + rally_result_dir_path = f.readline() + crucible_result_dir = rally_result_dir_path + "/crucible/results" + os.makedirs(crucible_result_dir) + self.copy_crucible_results(hostname_or_ip, user, password, crucible_result_dir) diff --git a/rally/rally-plugins/workloads/crucible-uperf.yml b/rally/rally-plugins/workloads/crucible-uperf.yml new file mode 100644 index 000000000..da417fa0a --- /dev/null +++ b/rally/rally-plugins/workloads/crucible-uperf.yml @@ -0,0 +1,59 @@ +{% set sla_max_avg_duration = sla_max_avg_duration or 60 %} +{% set sla_max_failure = sla_max_failure or 0 %} +{% set sla_max_seconds = sla_max_seconds or 60 %} +{% set nova_api_version = nova_api_version or 2.74 %} +--- +BrowbeatPlugin.crucible_uperf: + - + args: + flavor: + name: '{{ flavor_name }}' + image: + name: '{{ image_name }}' + num_provider_networks: {{ num_provider_networks }} + server_client_pairs_per_network: {{ server_client_pairs_per_network }} + hostname_or_ip: "{{ hostname_or_ip }}" + user: "{{ user }}" + password: "{{ password }}" + ext_iface: "{{ ext_iface }}" + client_userenv: "{{ client_userenv }}" + tags: "{{ tags }}" + profile: "{{ profile_path }}" + num_samples: {{ num_samples }} + crucible_repo_dir_path: "{{ crucible_repo_dir_path }}" + metadata: + test_metadata: "true" + runner: + concurrency: {{ concurrency }} + times: {{ times }} + type: 'constant' + context: + users: + tenants: 1 + users_per_tenant: 8 + api_versions: + nova: + version: {{ nova_api_version }} + quotas: + neutron: + network: -1 + port: -1 + router: -1 + subnet: -1 + nova: + instances: -1 + cores: -1 + ram: -1 + crucible_provider_networks: + hostname_or_ip: "{{ hostname_or_ip }}" + user: "{{ user }}" + password: "{{ password }}" + ext_iface: "{{ ext_iface }}" + num_provider_networks: {{ num_provider_networks }} + cidr_prefix: "{{ cidr_prefix }}" + provider_phys_net: "{{ provider_phys_net }}" + sla: + max_avg_duration: {{sla_max_avg_duration}} + max_seconds_per_iteration: {{sla_max_seconds}} + failure_rate: + max: {{sla_max_failure}} diff --git a/rally/rally-plugins/workloads/crucible/README.rst b/rally/rally-plugins/workloads/crucible/README.rst new file mode 100644 index 000000000..7d07871c1 --- /dev/null +++ b/rally/rally-plugins/workloads/crucible/README.rst @@ -0,0 +1,62 @@ +Browbeat-Crucible Network Workload +=================================== + +Introduction +------------ + +This document provides instructions for running the Browbeat-Crucible Network workload, which is designed to test network performance using Crucible remotehost endpoint. + +Prerequisites +------------- + +Before running the workload, ensure the following prerequisites are met: + +1. **Crucible Controller Setup**: Set up a Crucible controller on a bare metal host by following the instructions provided in the repository: `crucible `_. This involves cloning the repository and executing the installation script which is a zero touch installation. + +2. **Masquerade Rule Configuration**: Enable masquerade rule on the host using the following commands: + + sudo sysctl -w net.ipv4.ip_forward=1 + + sudo iptables -t nat -A POSTROUTING -o $(sudo ip r | grep default | awk '{print $5}') -j MASQUERADE + +3. **Paramiko Installation**: Install Paramiko version 3.2.0 within the Rally virtual environment. Incorrect Paramiko versions may lead to SSH issues with VMs. Activate the virtual environment and install Paramiko as follows: + + cd browbeat + + source .rally-venv/bin/activate + + pip3 uninstall paramiko + + pip3 install paramiko==3.2.0 + + deactivate + +4. **Image Setup**: Download a RHEL qcow2 image and upload it to Glance. Ensure necessary rhel repos are copied to the directory ``/home/stack/crucible_repos``. + +Running the Workload +--------------------- + +Follow these steps to run the Browbeat-Crucible Network workload: + +1. Edit ``browbeat-config.yaml`` to enable the Network workload and configure parameters according to your requirements. Adjust the parameters under the ``Network`` section, such as ``num_provider_networks``, ``cidr_prefix``, ``server_client_pairs_per_network``, ``hostname_or_ip``, ``user``, ``password``, ``crucible_repo_dir_path``, and ``ext_iface``. + +2. If you have a custom traffic profile, copy it to the directory ``~/browbeat/rally/rally-plugins/workloads/crucible`` as ``mv_params.json``. + +3. Execute the workload + + ./browbeat.py rally + +Parameters +----------- + +- **num_provider_networks**: Number of provider networks to be created for VM attachment. +- **cidr_prefix**: CIDR prefix used for the provider networks. +- **server_client_pairs_per_network**: Number of server and client pairs created per network. +- **hostname_or_ip**: Crucible controller hostname or IP address. +- **user**: Username for Crucible controller (root required). +- **password**: Root password for Crucible controller. +- **crucible_repo_dir_path**: Path to the directory containing ``.repo`` files specific to the image. +- **ext_iface**: External interface of Crucible controller with connectivity to provider networks. + +Adjust these parameters according to your specific requirements. + diff --git a/rally/rally-plugins/workloads/crucible/ansible/browbeat_bootstrap.yaml b/rally/rally-plugins/workloads/crucible/ansible/browbeat_bootstrap.yaml new file mode 100644 index 000000000..1e5b38ab6 --- /dev/null +++ b/rally/rally-plugins/workloads/crucible/ansible/browbeat_bootstrap.yaml @@ -0,0 +1,27 @@ +--- +- hosts: instances + gather_facts: false + remote_user: cloud-user + become: true + vars: + ansible_ssh_common_args: '-o StrictHostKeyChecking=no' + tasks: + - name: copy dns server + copy: + src: /etc/resolv.conf + dest: /etc/resolv.conf + mode: 0644 + + - name: Copy .repo files + copy: + src: "{{ item }}" + dest: /etc/yum.repos.d/ + mode: 0644 + with_fileglob: + - /root/*.repo + + - name: copy .ssh dir to root + ansible.builtin.shell: cp -r /home/cloud-user/.ssh/ /root/ && chmod -R 0600 /root/.ssh/ + + - name: install podman and wget + ansible.builtin.shell: yum install wget -y && yum install podman -y diff --git a/rally/rally-plugins/workloads/crucible/mv_params.json b/rally/rally-plugins/workloads/crucible/mv_params.json new file mode 100644 index 000000000..b1a8d8b80 --- /dev/null +++ b/rally/rally-plugins/workloads/crucible/mv_params.json @@ -0,0 +1,22 @@ +{ + "global-options": [ + { + "name": "common-params", + "params": [ + { "arg": "duration", "vals": ["60"] }, + { "arg": "protocol", "vals": ["udp"] }, + { "arg": "nthreads", "vals": ["1"] }, + { "arg": "ifname", "vals": ["eth0"], "role": "server" } + ] + } + ], + "sets": [ + { + "include": "common-params", + "params": [ + { "arg": "test-type", "vals": ["stream","rr"] }, + { "arg": "wsize", "vals": ["64"] } + ] + } + ] +} diff --git a/rally/rally-plugins/workloads/crucible/templates/uperf_script.sh.j2 b/rally/rally-plugins/workloads/crucible/templates/uperf_script.sh.j2 new file mode 100644 index 000000000..6ecc8f338 --- /dev/null +++ b/rally/rally-plugins/workloads/crucible/templates/uperf_script.sh.j2 @@ -0,0 +1,16 @@ +#!/bin/bash + +{{ name_ip_mappings_str }} + +client_userenv="{{ client_userenv }}" +tags="{{ tags }}" +profile="{{ profile }}" +num_samples={{ num_samples }} + +time crucible run uperf \ + --test-order r \ + --mv-params $profile \ + --tags "${tags}" \ + --num-samples $num_samples \ + --max-sample-failures 1 \ +{{ endpoint_str }} diff --git a/rally/rally-plugins/workloads/crucible/templates/vlans.sh.j2 b/rally/rally-plugins/workloads/crucible/templates/vlans.sh.j2 new file mode 100644 index 000000000..b66249047 --- /dev/null +++ b/rally/rally-plugins/workloads/crucible/templates/vlans.sh.j2 @@ -0,0 +1,54 @@ +#!/bin/bash + +# Check if script is run as root +if [ "$EUID" -ne 0 ]; then + echo "Please run as root (with sudo)." + exit 1 +fi + +# Check if action is provided +if [ $# -eq 0 ]; then + echo "Usage: $0 [create|delete]" + exit 1 +fi + +action=$1 # First command-line argument + +# Set variables +ext_iface="{{ ext_iface }}" +vlan_base_id=1 +num_vlans={{ num_vlans }} +cidr_prefix="{{ cidr_prefix }}" + +if [ "$action" == "create" ]; then + # Create VLAN interfaces + ip link set dev $ext_iface up + for ((i=0; i<$num_vlans; i++)); do + vlan_id=$((vlan_base_id + i)) + vlan_interface="${ext_iface}.${vlan_id}" + + ip link add link $ext_iface name $vlan_interface type vlan id $vlan_id + ip link set dev $vlan_interface up + + external_gateway="${cidr_prefix}.$((vlan_base_id + i)).1/24" + ip addr add $external_gateway dev $vlan_interface + + echo "VLAN $vlan_id configuration completed successfully." + done + echo "All configurations completed successfully." +elif [ "$action" == "delete" ]; then + # Delete VLAN interfaces + for ((i=0; i<$num_vlans; i++)); do + vlan_id=$((vlan_base_id + i)) + vlan_interface="${ext_iface}.${vlan_id}" + + ip link set dev $vlan_interface down + ip link delete $vlan_interface + + echo "VLAN $vlan_id removed successfully." + done + echo "Cleanup completed successfully." +else + echo "Invalid action. Please provide 'create' or 'delete' as the argument." + exit 1 +fi diff --git a/rally/rally-plugins/workloads/provider_net_context.py b/rally/rally-plugins/workloads/provider_net_context.py new file mode 100644 index 000000000..89a920cff --- /dev/null +++ b/rally/rally-plugins/workloads/provider_net_context.py @@ -0,0 +1,239 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from rally.task import context +from rally.common import logging +from rally.common import utils +from rally import consts +from rally_openstack.common import osclients +from rally_openstack.common.wrappers import network as network_wrapper + +import os +from jinja2 import Environment +from jinja2 import FileSystemLoader +import paramiko + +LOG = logging.getLogger(__name__) + +@context.configure(name="crucible_provider_networks", order=1000) +class CreateExternalNetworksContext(context.Context): + """This plugin creates provider networks with specified option.""" + + CONFIG_SCHEMA = { + "type": "object", + "$schema": consts.JSON_SCHEMA, + "additionalProperties": False, + "properties": { + "cidr_prefix": { + "type": "string", + }, + "num_provider_networks": { + "type": "integer", + "minimum": 0 + }, + "provider_phys_net": { + "type": "string" + }, + "hostname_or_ip": { + "type": "string" + }, + "user": { + "type": "string" + }, + "password": { + "type": "string" + }, + "ext_iface": { + "type": "string" + } + } + } + + def _create_subnet(self, tenant_id, network_id, network_number): + """Create subnet for provider network + + :param tenant_id: ID of tenant + :param network_id: ID of provider network + :param network_number: int, number for CIDR of subnet + :returns: subnet object + """ + + subnet_args = { + "subnet": { + "tenant_id": tenant_id, + "network_id": network_id, + "name": self.net_wrapper.owner.generate_random_name(), + "ip_version": 4, + "cidr": "{}.{}.0/24".format(self.cidr_prefix, network_number), + "enable_dhcp": True, + "gateway_ip": "{}.{}.1".format(self.cidr_prefix, network_number), + "allocation_pools": [{"start": "{}.{}.2".format(self.cidr_prefix, network_number), + "end": "{}.{}.254".format( + self.cidr_prefix, network_number)}] + } + } + return self.net_wrapper.client.create_subnet(subnet_args)["subnet"] + + def get_neutron_client(self): + return osclients.Clients(self.context["admin"]["credential"]).neutron() + + def run_ssh_script(self, host, username, password, script_path , action): + try: + with paramiko.SSHClient() as ssh: + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh.connect(host, username=username, password=password) + _, stdout, stderr = ssh.exec_command(f"bash {script_path} {action}") + + # Check the exit status of the command + if stdout.channel.recv_exit_status() == 0: + LOG.info(f"Script on {host} executed successfully.") + return True + else: + raise Exception( + f"Script execution failed on {host}. Error: {stderr.read().decode()}" + ) + except Exception as e: + raise Exception(f"Error in run_ssh_script: {e}") + + def render_template(self, env, template, ext_iface, num_vlans, cidr_prefix, + output_file_path): + rendered_template = template.render( + ext_iface=ext_iface, + num_vlans=num_vlans, + cidr_prefix=cidr_prefix + ) + + with open(output_file_path, 'w') as file: + file.write(rendered_template) + + def setup(self): + """This method is called before the task starts.""" + self.net_wrapper = network_wrapper.wrap( + osclients.Clients(self.context["admin"]["credential"]), + self, + config=self.config, + ) + self.context["provider_networks"] = [] + self.context["provider_subnets"] = {} + self.num_provider_networks = self.config.get("num_provider_networks", 16) + self.cidr_prefix = self.config.get("cidr_prefix", "192.18") + self.hostname_or_ip = self.config.get("hostname_or_ip", "hostname.example.com") + self.user = self.config.get("user", "root") + self.password = self.config.get("password", "passwd") + self.ext_iface = self.config.get("ext_iface", "ens2f1np1") + num_provider_networks_created = 0 + + while num_provider_networks_created < self.num_provider_networks: + has_error_occured = False + for user, tenant_id in utils.iterate_per_tenants( + self.context.get("users", []) + ): + try: + kwargs = { + "network_create_args": { + "provider:network_type": "vlan", + "provider:physical_network": self.config.get("provider_phys_net", + "datacentre"), + "provider:segmentation_id": num_provider_networks_created + 1, + "router:external": False, + "port_security_enabled": False + } + } + self.context["provider_networks"].append( + self.net_wrapper.create_network(tenant_id, **kwargs) + ) + LOG.debug( + "Provider network with id '%s' created as part of context" + % self.context["provider_networks"][-1]["id"] + ) + # update network name + updated_name = 'provider_' + str(num_provider_networks_created + 1) + cmd = f"source /home/stack/overcloudrc && openstack network set --name " \ + f"{updated_name} {self.context['provider_networks'][-1]['id']}" + os.system(cmd) + self.context["provider_networks"][-1]["name"] = updated_name + + num_provider_networks_created += 1 + except Exception as e: + msg = "Can't create provider network {} as part of context: {}".format( + num_provider_networks_created, e + ) + LOG.exception(msg) + has_error_occured = True + break + + try: + subnet = self._create_subnet(tenant_id, + self.context["provider_networks"][-1]["id"], + num_provider_networks_created) + self.context["provider_subnets"][ + self.context["provider_networks"][-1]["id"]] = subnet + LOG.debug( + "provider subnet with id '%s' created as part of context" + % subnet["id"] + ) + except Exception as e: + msg = "Can't create provider subnet {} as part of context: {}".format( + num_provider_networks_created, e + ) + LOG.exception(msg) + has_error_occured = True + break + + if has_error_occured: + break + + # prepare the script + env = Environment(loader=FileSystemLoader(os.getcwd())) + template_path = './rally/rally-plugins/workloads/crucible/templates/vlans.sh.j2' + template = env.get_template(template_path) + self.output_file_path = './rally/rally-plugins/workloads/crucible/browbeat_vlans.sh' + self.render_template(env, template, self.ext_iface, self.num_provider_networks, + self.cidr_prefix, self.output_file_path) + + # copy over scp + scp_command = ( + f"sshpass -p '{self.password}' scp {self.output_file_path} " + f"{self.user}@{self.hostname_or_ip}:/root/" + ) + os.system(scp_command) + # create vlan interfaces + self.run_ssh_script(self.hostname_or_ip, self.user, self.password, + "/root/browbeat_vlans.sh", "create") + + def cleanup(self): + """This method is called after the task finishes.""" + for i in range(self.num_provider_networks): + try: + provider_net = self.context["provider_networks"][i] + provider_net_id = provider_net["id"] + provider_subnet = self.context["provider_subnets"][provider_net_id] + provider_subnet_id = provider_subnet["id"] + self.net_wrapper._delete_subnet(provider_subnet_id) + LOG.debug( + "Provider subnet with id '%s' deleted from context" + % provider_subnet_id + ) + self.net_wrapper.delete_network(provider_net) + LOG.debug( + "Provider network with id '%s' deleted from context" + % provider_net_id + ) + except Exception as e: + msg = "Can't delete provider network {} from context: {}".format( + provider_net_id, e + ) + LOG.warning(msg) + + # delete vlan interfaces + self.run_ssh_script(self.hostname_or_ip, self.user, self.password, + "/root/browbeat_vlans.sh", "delete")