Skip to content

Commit 1b715fd

Browse files
Zuulopenstack-gerrit
authored andcommitted
Merge "Add ngs-stress test script"
2 parents 9c9fe62 + 3cb7da6 commit 1b715fd

File tree

3 files changed

+268
-0
lines changed

3 files changed

+268
-0
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
other:
3+
- |
4+
Adds a stress testing script, ``ngs-stress.py``, in the
5+
``tools/ngs-stress`` directory of the source code repository.

tools/ngs-stress/README.rst

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
=====================================
2+
Networking-generic-switch Stress Test
3+
=====================================
4+
5+
Stress test for the OpenStack Neutron networking-generic-switch (genericswitch)
6+
ML2 mechanism driver.
7+
8+
This script can stress a switch using the genericswitch driver. It does not
9+
require an OpenStack or Neutron installation, and can operate in isolation.
10+
There are two modes of operation:
11+
12+
network
13+
Create and delete a number of networks in parallel.
14+
port
15+
Create and delete a number of ports in parallel.
16+
17+
It is possible to use an existing genericswitch configuration file containing
18+
switch configuration.
19+
20+
Installation
21+
============
22+
23+
To install dependencies in a virtualenv::
24+
25+
python3 -m venv venv
26+
source venv/bin/activate
27+
pip install -U pip
28+
pip install -c https://releases.openstack.org/constraints/upper/<release> networking-generic-switch
29+
30+
If you want to use etcd for coordination, install the ``etcd3gw`` package::
31+
32+
pip install -c https://releases.openstack.org/constraints/upper/<release> etcd3gw
33+
34+
The Bitnami ``Etcd`` container can be used in a standalone mode::
35+
36+
docker run --detach -it -e ALLOW_NONE_AUTHENTICATION=yes --name Etcd --net=host bitnami/etcd
37+
38+
Configuration
39+
=============
40+
41+
A configuration file is required to provide details of switch devices, as well
42+
as any other NGS config options necessary. For example, to use the Fake device
43+
driver for testing:
44+
45+
.. code-block:: ini
46+
47+
[genericswitch:fake]
48+
device_type = netmiko_fake
49+
50+
Other drivers will typically require further configuration.
51+
52+
If you want to use etcd for coordination, add the following:
53+
54+
.. code-block:: ini
55+
56+
[ngs_coordination]
57+
backend_url = etcd3+http://localhost:2379?api_version=v3
58+
59+
Usage
60+
=====
61+
62+
To run the stress test in network mode::
63+
64+
venv/bin/python /path/to/ngs/tools/ngs-stress/ngs_stress.py \
65+
--config-file /path/to/ngs-stress.conf \
66+
--mode network \
67+
--switch <switch name> \
68+
--vlan-range <min>:<max>
69+
70+
To run the stress test in port mode::
71+
72+
venv/bin/python /path/to/ngs/tools/ngs-stress/ngs_stress.py \
73+
--config-file /path/to/ngs-stress.conf \
74+
--mode port \
75+
--switch <switch name> \
76+
--vlan-range <vlan>:<vlan+1> \
77+
--ports <port1,port2...>
78+
79+
Other arguments are available, see ``--help``.

tools/ngs-stress/ngs_stress.py

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
# Copyright (c) 2023 StackHPC Ltd.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
4+
# not use this file except in compliance with the License. You may obtain
5+
# a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12+
# License for the specific language governing permissions and limitations
13+
# under the License.
14+
15+
import contextlib
16+
import queue
17+
import sys
18+
import threading
19+
import uuid
20+
21+
import eventlet
22+
from neutron_lib.utils import net
23+
from oslo_config import cfg
24+
import oslo_log.log as logging
25+
26+
import networking_generic_switch.generic_switch_mech as generic_switch
27+
28+
eventlet.monkey_patch()
29+
30+
CONF = cfg.CONF
31+
LOG = logging.getLogger(__name__)
32+
33+
34+
OPTS = [
35+
cfg.HostAddressOpt('host', default=net.get_hostname(),
36+
help="Hostname to be used by NGS"),
37+
cfg.StrOpt('mode', choices=['network', 'port'], required=True,
38+
help='Mode of operation / resource type to exercise'),
39+
cfg.StrOpt('ports',
40+
help='Comma-separated list of ports to create in port mode.'),
41+
cfg.StrOpt('switch', required=True,
42+
help='Name of switch to connect to'),
43+
cfg.StrOpt('vlan-range', required=True,
44+
help='Colon-separated range of vlan IDs to create in network '
45+
'mode. In port mode the first will be used'),
46+
cfg.BoolOpt('create-net', default=True,
47+
help='Whether to create and delete a network in port mode'),
48+
cfg.StrOpt('net-id', required=False,
49+
help='Network UUID when create-net is false in port mode'),
50+
cfg.IntOpt('iterations', default=1,
51+
help='Number of test iterations'),
52+
]
53+
54+
55+
class ErrorQueueingThread(threading.Thread):
56+
"""Thread subclass which pushes a raised exception onto a queue."""
57+
58+
def __init__(self, target=None, eq=None, *args, **kwargs):
59+
def new_target(*t_args, **t_kwargs):
60+
with self.exceptions_queued(eq):
61+
return target(*t_args, **t_kwargs)
62+
super(ErrorQueueingThread, self).__init__(*args, target=new_target,
63+
**kwargs)
64+
65+
@contextlib.contextmanager
66+
def exceptions_queued(self, eq):
67+
try:
68+
yield
69+
except Exception:
70+
eq.put(sys.exc_info())
71+
raise
72+
73+
74+
def _log_excs_and_reraise(eq, reraise=True):
75+
"""Log all exceptions in a Queue and reraise one."""
76+
while not eq.empty():
77+
e = eq.get()
78+
LOG.error("Exception seen during test", exc_info=e)
79+
if reraise and eq.empty():
80+
raise e[0]
81+
82+
83+
def _run_threads(ts):
84+
"""Start a list of threads then wait for them to complete."""
85+
for t in ts:
86+
t.start()
87+
for t in ts:
88+
t.join()
89+
90+
91+
def _gen_net_id():
92+
"""Dell Force10 switches can't handle net names beginning with a letter."""
93+
while True:
94+
net_id = str(uuid.uuid4())
95+
try:
96+
int(net_id[0])
97+
except ValueError:
98+
return net_id
99+
100+
101+
def _create_net(switch, vlan, net_id):
102+
LOG.info("Creating VLAN %d", vlan)
103+
switch.add_network(vlan, net_id)
104+
105+
106+
def _delete_net(switch, vlan, net_id):
107+
LOG.info("Deleting VLAN %d", vlan)
108+
switch.del_network(vlan, net_id)
109+
110+
111+
def _create_delete_net(switch, vlan, net_id):
112+
"""Create and delete a VLAN."""
113+
_create_net(switch, vlan, net_id)
114+
_delete_net(switch, vlan, net_id)
115+
116+
117+
def _create_delete_nets(switch, vlans):
118+
"""Create and delete VLANs in parallel."""
119+
ts = []
120+
eq = queue.Queue()
121+
for vlan in vlans:
122+
args = (switch, vlan, _gen_net_id())
123+
t = ErrorQueueingThread(target=_create_delete_net, args=args,
124+
name='vlan-%d' % vlan, eq=eq)
125+
ts.append(t)
126+
_run_threads(ts)
127+
_log_excs_and_reraise(eq)
128+
129+
130+
def _add_remove_port(switch, port_id, vlan):
131+
"""Add and remove a port to/from a VLAN."""
132+
LOG.info("Adding port %s to VLAN %d", port_id, vlan)
133+
switch.plug_port_to_network(port_id, vlan)
134+
LOG.info("Removing port %s from VLAN %d", port_id, vlan)
135+
switch.delete_port(port_id, vlan)
136+
137+
138+
def _add_remove_ports(switch, ports, vlan):
139+
"""Add and remove ports to/from a VLAN in parallel."""
140+
ts = []
141+
eq = queue.Queue()
142+
if CONF.create_net:
143+
net_id = _gen_net_id()
144+
_create_net(switch, vlan, net_id)
145+
else:
146+
net_id = CONF.net_id
147+
for port_id in ports:
148+
args = (switch, port_id, vlan)
149+
t = ErrorQueueingThread(target=_add_remove_port, args=args,
150+
name='port-%s' % port_id, eq=eq)
151+
ts.append(t)
152+
_run_threads(ts)
153+
if CONF.create_net:
154+
_delete_net(switch, vlan, net_id)
155+
_log_excs_and_reraise(eq, reraise=False)
156+
157+
158+
def _init():
159+
logging.register_options(CONF)
160+
CONF.register_cli_opts(OPTS)
161+
CONF(sys.argv[1:])
162+
logging.setup(CONF, 'ngs_stress')
163+
LOG.info("Starting NGS stress test")
164+
165+
166+
def main():
167+
_init()
168+
gs = generic_switch.GenericSwitchDriver()
169+
gs.initialize()
170+
switch = gs.switches[CONF.switch]
171+
vlans = range(*map(int, CONF.vlan_range.split(':')))
172+
if CONF.mode == 'network':
173+
for _ in range(CONF.iterations):
174+
_create_delete_nets(switch, vlans)
175+
else:
176+
vlan = vlans[0]
177+
ports = CONF.ports.split(',')
178+
for _ in range(CONF.iterations):
179+
_add_remove_ports(switch, ports, vlan)
180+
LOG.info("NGS stress test complete")
181+
182+
183+
if __name__ == "__main__":
184+
main()

0 commit comments

Comments
 (0)