diff --git a/ansible/install/roles/browbeat/tasks/main.yml b/ansible/install/roles/browbeat/tasks/main.yml index 8b078797c..e3a7bc168 100644 --- a/ansible/install/roles/browbeat/tasks/main.yml +++ b/ansible/install/roles/browbeat/tasks/main.yml @@ -82,6 +82,13 @@ - debug: msg="Browbeat directory exists already." when: browbeat_exists.stat.isdir is defined and browbeat_exists.stat.isdir +- name: install scapy + pip: + name: "scapy" + state: latest + become: true + when: ansible_distribution_major_version >= '8' + - name: Clone browbeat on undercloud git: repo: https://github.com/openstack/browbeat.git diff --git a/browbeat-config.yaml b/browbeat-config.yaml index 4a3134c5b..71b677880 100644 --- a/browbeat-config.yaml +++ b/browbeat-config.yaml @@ -577,12 +577,18 @@ workloads: num_add_subports: 1 num_delete_subports_trunks: 1 num_delete_subports: 1 + provider_phys_net: "provider1" + iface_name: "ens7f0" + iface_mac: "3c:fd:fe:c1:73:40" + num_vms_provider_net: 2 # workloads can be 'all', a single workload(Eg. : create_delete_servers), # or a comma separated string(Eg. : create_delete_servers,migrate_servers). # Currently supported workloads : create_delete_servers, migrate_servers - # create_loadbalancers, pod_fip_simulation, add_subports_to_random_trunks, - # delete_subports_from_random_trunks, delete_loadbalancers, delete_members_random_lb - # Note: Octavia scenarios are not included in 'all' by default, and have - # to be included separately. + # create_loadbalancers, delete_loadbalancers, delete_members_random_lb, + # pod_fip_simulation, add_subports_to_random_trunks, + # delete_subports_from_random_trunks, provider_netcreate_nova_boot_ping, + # provider_net_nova_boot_ping, provider_net_nova_delete + # Note: Octavia and Provider scenarios are not included in 'all' by default, + # and have to be included separately. workloads: all file: rally/rally-plugins/dynamic-workloads/dynamic_workload.yml diff --git a/rally/rally-plugins/dynamic-workloads/README.rst b/rally/rally-plugins/dynamic-workloads/README.rst index ddf940d33..33bd73052 100644 --- a/rally/rally-plugins/dynamic-workloads/README.rst +++ b/rally/rally-plugins/dynamic-workloads/README.rst @@ -32,3 +32,6 @@ Functions: - _boot_server_with_tag: Boot a server with a tag - _boot_server_with_fip_and_tag: Boot server prepared for SSH actions, with tag - _get_servers_by_tag: Retrieve list of servers based on tag +- provider_netcreate_nova_boot_ping: Creates a provider Network and Boots VM and ping +- provider_net_nova_boot_ping: Boots a VM and ping on random existing provider network +- provider_net_nova_delete: Delete all VM's and provider network diff --git a/rally/rally-plugins/dynamic-workloads/dynamic_workload.py b/rally/rally-plugins/dynamic-workloads/dynamic_workload.py index c349c5f3b..8db71037f 100644 --- a/rally/rally-plugins/dynamic-workloads/dynamic_workload.py +++ b/rally/rally-plugins/dynamic-workloads/dynamic_workload.py @@ -17,6 +17,7 @@ from rally.task import validation import vm import trunk import octavia +import provider_network @types.convert(octavia_image={"type": "glance_image"}, octavia_flavor={"type": "nova_flavor"}) @@ -43,13 +44,14 @@ import octavia platform="openstack", ) class DynamicWorkload(vm.VMDynamicScenario, trunk.TrunkDynamicScenario, - octavia.DynamicOctaviaBase): + octavia.DynamicOctaviaBase, provider_network.DynamicProviderNetworkBase): def run( self, smallest_image, smallest_flavor, ext_net_id, num_vms_to_create_for_migration, num_vms_to_migrate, trunk_image, trunk_flavor, num_initial_subports, num_trunk_vms, num_add_subports, num_add_subports_trunks, num_delete_subports, num_delete_subports_trunks, - octavia_image, octavia_flavor, user, user_data_file, num_lbs, num_pools, - num_clients, delete_num_lbs, delete_num_members, num_create_delete_vms, workloads="all", + octavia_image, octavia_flavor, user, user_data_file, num_lbs, num_pools, num_clients, + delete_num_lbs, delete_num_members, num_create_delete_vms, provider_phys_net, + iface_name, iface_mac, num_vms_provider_net, workloads="all", router_create_args=None, network_create_args=None, subnet_create_args=None, **kwargs): @@ -85,3 +87,15 @@ class DynamicWorkload(vm.VMDynamicScenario, trunk.TrunkDynamicScenario, if "delete_members_random_lb" in workloads_list: self.delete_members_random_lb(delete_num_members) + + if "provider_netcreate_nova_boot_ping" in workloads_list: + self.provider_netcreate_nova_boot_ping(smallest_image, smallest_flavor, + provider_phys_net, iface_name, + iface_mac, num_vms_provider_net) + + if "provider_net_nova_boot_ping" in workloads_list: + self.provider_net_nova_boot_ping(provider_phys_net, iface_name, iface_mac, + smallest_image, smallest_flavor) + + if "provider_net_nova_delete" in workloads_list: + self.provider_net_nova_delete(provider_phys_net) diff --git a/rally/rally-plugins/dynamic-workloads/dynamic_workload.yml b/rally/rally-plugins/dynamic-workloads/dynamic_workload.yml index 4ef2ca6b8..2112eec38 100644 --- a/rally/rally-plugins/dynamic-workloads/dynamic_workload.yml +++ b/rally/rally-plugins/dynamic-workloads/dynamic_workload.yml @@ -60,6 +60,10 @@ BrowbeatPlugin.dynamic_workload: num_create_delete_vms: {{num_create_delete_vms}} num_vms_to_create_for_migration: {{num_vms_to_create_for_migration}} num_vms_to_migrate: {{num_vms_to_migrate}} + provider_phys_net: '{{ provider_phys_net }}' + iface_name: '{{ iface_name }}' + iface_mac: '{{ iface_mac }}' + num_vms_provider_net: {{ num_vms_provider_net }} ext_net_id: '{{ext_net_id}}' workloads: '{{workloads}}' runner: diff --git a/rally/rally-plugins/dynamic-workloads/provider_network.py b/rally/rally-plugins/dynamic-workloads/provider_network.py new file mode 100644 index 000000000..72ca8e213 --- /dev/null +++ b/rally/rally-plugins/dynamic-workloads/provider_network.py @@ -0,0 +1,152 @@ +# 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. + +import logging +import random +import os +import subprocess + +from rally_openstack.scenarios.neutron import utils as neutron_utils +import dynamic_utils +from rally.task import atomic + +LOG = logging.getLogger(__name__) + + +class DynamicProviderNetworkBase(dynamic_utils.NovaUtils, neutron_utils.NeutronScenario): + + @atomic.action_timer("neutron.create_network") + def _create_provider_network(self, provider_phys_net): + """Create neutron provider network. + :param provider_phys_net: provider physical network + :returns: neutron network dict + """ + project_id = self.context["tenant"]["id"] + body = { + "name": self.generate_random_name(), + "tenant_id": project_id, + "provider:network_type": "vlan", + "provider:physical_network": provider_phys_net + } + # provider network can be created by admin client only + return self.admin_clients("neutron").create_network({"network": body}) + + @atomic.action_timer("neutron.show_network") + def _show_provider_network(self, provider_network): + """Fetches information of a certain provider network. + :param provider_network: provider network object + """ + + return self.admin_clients("neutron").show_network(provider_network['network']['id']) + + @atomic.action_timer("neutron.delete_network") + def _delete_provider_network(self, provider_network): + """Delete neutron provider network. + :param provider_network: provider network object + """ + + return self.admin_clients("neutron").delete_network(provider_network['network']['id']) + + def ping_server(self, server, iface_name, iface_mac, network, subnet): + """Ping server created on provider network + :param server: server object + :param iface_name: interface name + :param iface_mac: interface MAC + :param network: provider network object + :param subnet: subnet object + """ + + internal_network = list(server.networks)[0] + server_ip = server.addresses[internal_network][0]["addr"] + server_mac = server.addresses[internal_network][0]["OS-EXT-IPS-MAC:mac_addr"] + gateway = subnet['subnet']['gateway_ip'] + vlan = network['network']['provider:segmentation_id'] + dir_path = os.path.dirname(os.path.realpath(__file__)) + file_path = os.path.join(dir_path, "scapy_icmp.py") + cmd = ["sudo", file_path, server_ip, server_mac, gateway, iface_mac, iface_name, str(vlan)] + proc = subprocess.Popen(cmd) + proc.wait() + if proc.returncode == 0: + LOG.info("Ping to {} is successful".format(server_ip)) + else: + LOG.info("Ping to {} is failed".format(server_ip)) + + def provider_netcreate_nova_boot_ping(self, image, flavor, provider_phys_net, iface_name, + iface_mac, num_vms_provider_net, router_create_args=None, + network_create_args=None, + subnet_create_args=None, + **kwargs): + """Create provider network, Boot and Ping VM + :param image: image ID or image name + :param flavor: flavor ID or flavor name + :param provider_phys_net: provider physical network + :param iface_name: interface name + :param iface_mac: interface MAC + :param num_vms_provider_net: int, number of vm's to create + :param router_create_args: dict, arguments for router creation + :param network_create_args: dict, arguments for network creation + :param subnet_create_args: dict, arguments for subnet creation + :param kwargs: dict, Keyword arguments to function + """ + + for _ in range(num_vms_provider_net): + provider_network = self._create_provider_network(provider_phys_net) + subnet = self._create_subnet(provider_network, subnet_create_args or {}) + kwargs["nics"] = [{'net-id': provider_network['network']['id']}] + tag = "provider_network:"+str(provider_network['network']['id']) + server = self._boot_server_with_tag(image, flavor, tag, **kwargs) + LOG.info(" Server {} created on provider network {}".format(server, provider_network)) + self.ping_server(server, iface_name, iface_mac, provider_network, subnet) + + def pick_random_provider_net(self, provider_phys_net, **kwargs): + """Picks random provider network that exists + :param provider_phys_net: provider physical network + :param kwargs: dict, Keyword arguments to function + """ + + kwargs["provider:physical_network"] = provider_phys_net + nets = self._list_networks(**kwargs) + return self._show_provider_network({'network': random.choice(nets)}) + + def provider_net_nova_boot_ping(self, provider_phys_net, iface_name, + iface_mac, image, flavor, **kwargs): + """Boot a VM on a random provider network that exists and Ping + :param provider_phys_net: provider physical network + :param iface_name: interface name + :param iface_mac: interface MAC + :param image: image ID or image name + :param flavor: flavor ID or flavor name + :param kwargs: dict, Keyword arguments to function + """ + + random_network = self.pick_random_provider_net(provider_phys_net) + kwargs["nics"] = [{'net-id': random_network['network']['id']}] + tag = "provider_network:"+str(random_network['network']['id']) + server = self._boot_server_with_tag(image, flavor, tag, **kwargs) + subnet_id = random_network['network']['subnets'][0] + subnet = self.admin_clients("neutron").show_subnet(subnet_id) + self.ping_server(server, iface_name, iface_mac, random_network, subnet) + + def provider_net_nova_delete(self, provider_phys_net, **kwargs): + """Delete all the VM's on the provider network and then + the network. + :param provider_phys_net: provider physical network + :param kwargs: dict, Keyword arguments to function + """ + + random_network = self.pick_random_provider_net(provider_phys_net) + kwargs["nics"] = [{'net-id': random_network['network']['id']}] + tag = "provider_network:"+str(random_network['network']['id']) + servers = self._get_servers_by_tag(tag) + for server in servers: + self._delete_server(server) + self._delete_provider_network(random_network) diff --git a/rally/rally-plugins/dynamic-workloads/scapy_icmp.py b/rally/rally-plugins/dynamic-workloads/scapy_icmp.py new file mode 100755 index 000000000..95c5cb098 --- /dev/null +++ b/rally/rally-plugins/dynamic-workloads/scapy_icmp.py @@ -0,0 +1,64 @@ +#!/usr/bin/python3 +# 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. + +import logging +import sys +import time + +from scapy.all import srp +from scapy.all import Ether +from scapy.all import ARP +from scapy.all import sendp +from scapy.all import Dot1Q +from scapy.all import IP +from scapy.all import ICMP + + +LOG = logging.getLogger(__name__) + + +def _send_icmp(dst_ip, dst_mac, src_ip, src_mac, iface, vlan): + # send 1 icmp packet so that dest VM can send ARP packet to resolve gateway + sendp(Ether(dst=dst_mac, src=src_mac)/Dot1Q(vlan=int(vlan))/IP(dst=dst_ip)/ICMP(), + iface=iface, verbose=0) + bcast = "ff:ff:ff:ff:ff:ff" + # Send GARP using ARP reply method + sendp(Ether(dst=bcast,src=src_mac)/Dot1Q(vlan=int(vlan))/ARP( + op=2,psrc=src_ip, hwsrc=src_mac, hwdst=src_mac, pdst=src_ip), iface=iface, verbose=0) + # send ICMP and validate reply + ans, unans = srp(Ether(dst=dst_mac, src=src_mac)/Dot1Q(vlan=int(vlan))/IP(dst=dst_ip)/ICMP(), + iface=iface, timeout=5, verbose=0) + if (str(ans).find('ICMP:0') == -1): + for snd, rcv in ans: + if (rcv.summary().find(dst_ip) != -1): + LOG.info("Ping to {} is succesful".format(dst_ip)) + return True + return False + + +def main(args): + dst_ip, dst_mac, src_ip, src_mac, iface, vlan = args[1:] + attempts = 0 + max_attempts = 120 + while attempts < max_attempts: + if _send_icmp(dst_ip, dst_mac, src_ip, src_mac, iface, vlan): + LOG.info("Ping to {} is succesful".format(dst_ip)) + return 0 + LOG.info("Ping to {} is failed, attempt {}".format(dst_ip, attempts)) + attempts += 1 + time.sleep(5) + return 1 + + +if __name__ == "__main__": + main(sys.argv)