NFVBENCH-180: Add a 'no_e2e_check' option to skip "end to end" connectivity check
[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
75         # the only case we do not need to set the dest MAC is in the case of
76         # l2-loopback (because the traffic gen will default to use the peer MAC)
77         # or EXT+ARP+VLAN (because dest MAC will be discovered by TRex ARP)
78         # Note that in the case of EXT+ARP+VxLAN, the dest MACs need to be loaded
79         # because ARP only operates on the dest VTEP IP not on the VM dest MAC
80         if not config.l2_loopback and \
81                 (config.service_chain != ChainType.EXT or config.no_arp or config.vxlan):
82             gen_config.set_dest_macs(0, self.chain_manager.get_dest_macs(0))
83             gen_config.set_dest_macs(1, self.chain_manager.get_dest_macs(1))
84
85         if config.vxlan:
86             # VXLAN is discovered from the networks
87             vtep_vlan = gen_config.gen_config.vtep_vlan
88             src_vteps = gen_config.gen_config.src_vteps
89             dst_vtep = gen_config.gen_config.dst_vtep
90             gen_config.set_vxlans(0, self.chain_manager.get_chain_vxlans(0))
91             gen_config.set_vxlans(1, self.chain_manager.get_chain_vxlans(1))
92             gen_config.set_vtep_vlan(0, vtep_vlan)
93             gen_config.set_vtep_vlan(1, vtep_vlan)
94             # Configuring source an remote VTEPs on TREx interfaces
95             gen_config.set_vxlan_endpoints(0, src_vteps[0], dst_vtep)
96             gen_config.set_vxlan_endpoints(1, src_vteps[1], dst_vtep)
97             self.config['vxlan_gen_config'] = gen_config
98
99         if config.mpls:
100             # MPLS VPN is discovered from the networks
101             src_vteps = gen_config.gen_config.src_vteps
102             vtep_gateway_ips = gen_config.gen_config.vtep_gateway_ips
103             gen_config.set_mpls_inner_labels(0, self.chain_manager.get_chain_mpls_inner_labels(0))
104             gen_config.set_mpls_inner_labels(1, self.chain_manager.get_chain_mpls_inner_labels(1))
105             outer_mpls_labels_left = self.config.internal_networks.left.mpls_transport_labels
106             outer_mpls_labels_right = self.config.internal_networks.right.mpls_transport_labels
107             if outer_mpls_labels_left or outer_mpls_labels_right:
108                 gen_config.set_mpls_outer_labels(0, outer_mpls_labels_left)
109                 gen_config.set_mpls_outer_labels(1, outer_mpls_labels_right)
110             # Configuring source an remote VTEPs on TREx interfaces
111             gen_config.set_mpls_peers(0, src_vteps[0], vtep_gateway_ips[0])
112             gen_config.set_mpls_peers(1, src_vteps[1], vtep_gateway_ips[1])
113             self.config['mpls_gen_config'] = gen_config
114
115         # get an instance of the stats manager
116         self.stats_manager = StatsManager(self)
117         LOG.info('ChainRunner initialized')
118
119     def __setup_traffic(self):
120         # possibly skip connectivity check
121         if self.config.no_e2e_check:
122             LOG.info('Skipping end to end connectivity check')
123             return
124         self.traffic_client.setup()
125         if not self.config.no_traffic:
126             # ARP is needed for EXT chain or VxLAN overlay or MPLS unless disabled explicitly
127             if (self.config.service_chain == ChainType.EXT or self.config.mpls or
128                     self.config.vxlan or self.config.l3_router or self.config.loop_vm_arp)\
129                     and not self.config.no_arp:
130                 self.traffic_client.ensure_arp_successful()
131             self.traffic_client.ensure_end_to_end()
132
133     def __get_result_per_frame_size(self, frame_size, bidirectional):
134         traffic_result = {
135             frame_size: {}
136         }
137         result = {}
138         if not self.config.no_traffic:
139             self.traffic_client.set_traffic(frame_size, bidirectional)
140
141             if self.config.single_run:
142                 result = self.stats_manager.run_fixed_rate()
143             else:
144                 results = self.traffic_client.get_ndr_and_pdr()
145
146                 for dr in ['pdr', 'ndr']:
147                     if dr in results:
148                         traffic_result[frame_size][dr] = results[dr]
149                         if 'warning' in results[dr]['stats'] and results[dr]['stats']['warning']:
150                             traffic_result['warning'] = results[dr]['stats']['warning']
151                 traffic_result[frame_size]['iteration_stats'] = results['iteration_stats']
152
153             if self.config.single_run:
154                 result['run_config'] = self.traffic_client.get_run_config(result)
155                 required = result['run_config']['direction-total']['orig']['rate_pps']
156                 actual = result['stats']['total_tx_rate']
157                 warning = self.traffic_client.compare_tx_rates(required, actual)
158                 if warning is not None:
159                     result['run_config']['warning'] = warning
160
161         traffic_result[frame_size].update(result)
162         return traffic_result
163
164     def __get_chain_result(self):
165         result = OrderedDict()
166         for fs in self.config.frame_sizes:
167             result.update(self.__get_result_per_frame_size(fs,
168                                                            self.config.traffic.bidirectional))
169         chain_result = {
170             'flow_count': self.config.flow_count,
171             'service_chain_count': self.config.service_chain_count,
172             'bidirectional': self.config.traffic.bidirectional,
173             'profile': self.config.traffic.profile,
174             'compute_nodes': self.stats_manager.get_compute_nodes_bios(),
175             'result': result
176         }
177         return chain_result
178
179     def run(self):
180         """Run the requested benchmark.
181
182         return: the results of the benchmark as a dict
183         """
184         results = {}
185         if self.config.no_traffic:
186             return results
187
188         LOG.info('Starting %dx%s benchmark...', self.config.service_chain_count, self.chain_name)
189         self.stats_manager.create_worker()
190         if self.config.vxlan or self.config.mpls:
191             # Configure vxlan or mpls tunnels
192             self.stats_manager.worker.config_interfaces()
193         self.__setup_traffic()
194
195         results[self.chain_name] = {'result': self.__get_chain_result()}
196
197         LOG.info("Service chain '%s' run completed.", self.chain_name)
198         return results
199
200     def close(self):
201         """Close this instance of chain runner and delete resources if applicable."""
202         try:
203             if not self.config.no_cleanup:
204                 LOG.info('Cleaning up...')
205                 if self.chain_manager:
206                     self.chain_manager.delete()
207             else:
208                 LOG.info('Clean up skipped.')
209             try:
210                 self.traffic_client.close()
211             except Exception:
212                 LOG.exception()
213             if self.stats_manager:
214                 self.stats_manager.close()
215         except Exception:
216             LOG.exception('Cleanup not finished')
217
218     def get_version(self):
219         """Retrieve the version of dependent components."""
220         versions = {}
221         if self.traffic_client:
222             versions['Traffic_Generator'] = self.traffic_client.get_version()
223         versions.update(self.stats_manager.get_version())
224         return versions