NFVBENCH-163: Add gratuitous ARP in case of L3 router mode
[nfvbench.git] / nfvbench / chain_runner.py
1 #!/usr/bin/env python
2 # Copyright 2016 Cisco Systems, Inc.  All rights reserved.
3 #
4 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
5 #    not use this file except in compliance with the License. You may obtain
6 #    a copy of the License at
7 #
8 #         http://www.apache.org/licenses/LICENSE-2.0
9 #
10 #    Unless required by applicable law or agreed to in writing, software
11 #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 #    License for the specific language governing permissions and limitations
14 #    under the License.
15 #
16 """This module takes care of coordinating a benchmark run between various modules.
17
18 The ChainRunner class is in charge of coordinating:
19 - the chain manager which takes care of staging resources
20 - traffic generator client which drives the traffic generator
21 - the stats manager which collects and aggregates stats
22 """
23
24 from collections import OrderedDict
25
26 from .chaining import ChainManager
27 from .log import LOG
28 from .specs import ChainType
29 from .stats_manager import StatsManager
30 from .traffic_client import TrafficClient
31
32
33 class ChainRunner(object):
34     """Run selected chain, collect results and analyse them."""
35
36     def __init__(self, config, cred, specs, factory, notifier=None):
37         """Create a new instance of chain runner.
38
39         Create dependent components
40         A new instance is created everytime the nfvbench config may have changed.
41
42         config: the new nfvbench config to use for this run
43         cred: openstack credentials (or None if no openstack)
44         specs: TBD
45         factory:
46         notifier:
47         """
48         self.config = config
49         self.cred = cred
50         self.specs = specs
51         self.factory = factory
52         self.notifier = notifier
53         self.chain_name = self.config.service_chain
54
55         # get an instance of traffic client
56         self.traffic_client = TrafficClient(config, notifier)
57
58         if self.config.no_traffic:
59             LOG.info('Dry run: traffic generation is disabled')
60         else:
61             # Start the traffic generator server
62             self.traffic_client.start_traffic_generator()
63
64         # get an instance of a chain manager
65         self.chain_manager = ChainManager(self)
66
67         # at this point all resources are setup/discovered
68         # we need to program the traffic dest MAC and VLANs
69         gen_config = self.traffic_client.generator_config
70         if config.vlan_tagging:
71             # VLAN is discovered from the networks
72             gen_config.set_vlans(0, self.chain_manager.get_chain_vlans(0))
73             gen_config.set_vlans(1, self.chain_manager.get_chain_vlans(1))
74         else:
75             LOG.info("Ports: untagged")
76
77         # the only case we do not need to set the dest MAC is in the case of
78         # l2-loopback (because the traffic gen will default to use the peer MAC)
79         # or EXT+ARP+VLAN (because dest MAC will be discovered by TRex ARP)
80         # Note that in the case of EXT+ARP+VxLAN, the dest MACs need to be loaded
81         # because ARP only operates on the dest VTEP IP not on the VM dest MAC
82         if not config.l2_loopback and \
83                 (config.service_chain != ChainType.EXT or config.no_arp or config.vxlan):
84             gen_config.set_dest_macs(0, self.chain_manager.get_dest_macs(0))
85             gen_config.set_dest_macs(1, self.chain_manager.get_dest_macs(1))
86
87         if config.vxlan:
88             # VXLAN is discovered from the networks
89             vtep_vlan = gen_config.gen_config.vtep_vlan
90             src_vteps = gen_config.gen_config.src_vteps
91             dst_vtep = gen_config.gen_config.dst_vtep
92             gen_config.set_vxlans(0, self.chain_manager.get_chain_vxlans(0))
93             gen_config.set_vxlans(1, self.chain_manager.get_chain_vxlans(1))
94             gen_config.set_vtep_vlan(0, vtep_vlan)
95             gen_config.set_vtep_vlan(1, vtep_vlan)
96             # Configuring source an remote VTEPs on TREx interfaces
97             gen_config.set_vxlan_endpoints(0, src_vteps[0], dst_vtep)
98             gen_config.set_vxlan_endpoints(1, src_vteps[1], dst_vtep)
99             self.config['vxlan_gen_config'] = gen_config
100
101         if config.mpls:
102             # MPLS VPN is discovered from the networks
103             src_vteps = gen_config.gen_config.src_vteps
104             vtep_gateway_ips = gen_config.gen_config.vtep_gateway_ips
105             gen_config.set_mpls_inner_labels(0, self.chain_manager.get_chain_mpls_inner_labels(0))
106             gen_config.set_mpls_inner_labels(1, self.chain_manager.get_chain_mpls_inner_labels(1))
107             outer_mpls_labels_left = self.config.internal_networks.left.mpls_transport_labels
108             outer_mpls_labels_right = self.config.internal_networks.right.mpls_transport_labels
109             if outer_mpls_labels_left or outer_mpls_labels_right:
110                 gen_config.set_mpls_outer_labels(0, outer_mpls_labels_left)
111                 gen_config.set_mpls_outer_labels(1, outer_mpls_labels_right)
112             # Configuring source an remote VTEPs on TREx interfaces
113             gen_config.set_mpls_peers(0, src_vteps[0], vtep_gateway_ips[0])
114             gen_config.set_mpls_peers(1, src_vteps[1], vtep_gateway_ips[1])
115             self.config['mpls_gen_config'] = gen_config
116
117         # get an instance of the stats manager
118         self.stats_manager = StatsManager(self)
119         LOG.info('ChainRunner initialized')
120
121     def __setup_traffic(self):
122         # possibly skip connectivity check
123         if self.config.no_e2e_check:
124             LOG.info('Skipping end to end connectivity check')
125             return
126         self.traffic_client.setup()
127         if not self.config.no_traffic:
128             # ARP is needed for EXT chain or VxLAN overlay or MPLS unless disabled explicitly
129             if (self.config.service_chain == ChainType.EXT or self.config.mpls or
130                     self.config.vxlan or self.config.l3_router or self.config.loop_vm_arp)\
131                     and not self.config.no_arp:
132                 self.traffic_client.ensure_arp_successful()
133             self.traffic_client.ensure_end_to_end()
134
135     def __get_result_per_frame_size(self, frame_size, bidirectional):
136         traffic_result = {
137             frame_size: {}
138         }
139         result = {}
140         if not self.config.no_traffic:
141             self.traffic_client.set_traffic(frame_size, bidirectional)
142
143             if self.config.single_run:
144                 result = self.stats_manager.run_fixed_rate()
145             else:
146                 results = self.traffic_client.get_ndr_and_pdr()
147
148                 for dr in ['pdr', 'ndr']:
149                     if dr in results:
150                         traffic_result[frame_size][dr] = results[dr]
151                         if 'warning' in results[dr]['stats'] and results[dr]['stats']['warning']:
152                             traffic_result['warning'] = results[dr]['stats']['warning']
153                 traffic_result[frame_size]['iteration_stats'] = results['iteration_stats']
154
155             if self.config.single_run:
156                 result['run_config'] = self.traffic_client.get_run_config(result)
157                 required = result['run_config']['direction-total']['orig']['rate_pps']
158                 if self.config.periodic_gratuitous_arp:
159                     actual = result['stats']['total_tx_rate'] + self.config.gratuitous_arp_pps
160                 else:
161                     actual = result['stats']['total_tx_rate']
162                 warning = self.traffic_client.compare_tx_rates(required, actual)
163                 if warning is not None:
164                     result['run_config']['warning'] = warning
165
166         traffic_result[frame_size].update(result)
167         return traffic_result
168
169     def __get_chain_result(self):
170         result = OrderedDict()
171         for fs in self.config.frame_sizes:
172             result.update(self.__get_result_per_frame_size(fs,
173                                                            self.config.traffic.bidirectional))
174         chain_result = {
175             'flow_count': self.config.flow_count,
176             'service_chain_count': self.config.service_chain_count,
177             'bidirectional': self.config.traffic.bidirectional,
178             'profile': self.config.traffic.profile,
179             'compute_nodes': self.stats_manager.get_compute_nodes_bios(),
180             'result': result
181         }
182         return chain_result
183
184     def run(self):
185         """Run the requested benchmark.
186
187         return: the results of the benchmark as a dict
188         """
189         results = {}
190         if self.config.no_traffic:
191             return results
192
193         LOG.info('Starting %dx%s benchmark...', self.config.service_chain_count, self.chain_name)
194         self.stats_manager.create_worker()
195         if self.config.vxlan or self.config.mpls:
196             # Configure vxlan or mpls tunnels
197             self.stats_manager.worker.config_interfaces()
198         self.__setup_traffic()
199
200         results[self.chain_name] = {'result': self.__get_chain_result()}
201
202         LOG.info("Service chain '%s' run completed.", self.chain_name)
203         return results
204
205     def close(self):
206         """Close this instance of chain runner and delete resources if applicable."""
207         try:
208             if not self.config.no_cleanup:
209                 LOG.info('Cleaning up...')
210                 if self.chain_manager:
211                     self.chain_manager.delete()
212             else:
213                 LOG.info('Clean up skipped.')
214             try:
215                 self.traffic_client.close()
216             except Exception as exc:
217                 LOG.exception(exc)
218             if self.stats_manager:
219                 self.stats_manager.close()
220         except Exception:
221             LOG.exception('Cleanup not finished')
222
223     def get_version(self):
224         """Retrieve the version of dependent components."""
225         versions = {}
226         if self.traffic_client:
227             versions['Traffic_Generator'] = self.traffic_client.get_version()
228         versions.update(self.stats_manager.get_version())
229         return versions