NFVBENCH-108 Adjust exact frame size to take into account FCS field
[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 (because dest MAC will be discovered by TRex ARP)
78         if not config.l2_loopback and (config.service_chain != ChainType.EXT or config.no_arp):
79             gen_config.set_dest_macs(0, self.chain_manager.get_dest_macs(0))
80             gen_config.set_dest_macs(1, self.chain_manager.get_dest_macs(1))
81
82         # get an instance of the stats manager
83         self.stats_manager = StatsManager(self)
84         LOG.info('ChainRunner initialized')
85
86     def __setup_traffic(self):
87         self.traffic_client.setup()
88         if not self.config.no_traffic:
89             if self.config.service_chain == ChainType.EXT and not self.config.no_arp:
90                 self.traffic_client.ensure_arp_successful()
91             self.traffic_client.ensure_end_to_end()
92
93     def __get_result_per_frame_size(self, frame_size, bidirectional):
94         traffic_result = {
95             frame_size: {}
96         }
97         result = {}
98         if not self.config.no_traffic:
99             self.traffic_client.set_traffic(frame_size, bidirectional)
100
101             if self.config.single_run:
102                 result = self.stats_manager.run_fixed_rate()
103             else:
104                 results = self.traffic_client.get_ndr_and_pdr()
105
106                 for dr in ['pdr', 'ndr']:
107                     if dr in results:
108                         traffic_result[frame_size][dr] = results[dr]
109                         if 'warning' in results[dr]['stats'] and results[dr]['stats']['warning']:
110                             traffic_result['warning'] = results[dr]['stats']['warning']
111                 traffic_result[frame_size]['iteration_stats'] = results['iteration_stats']
112
113             if self.config.single_run:
114                 result['run_config'] = self.traffic_client.get_run_config(result)
115                 required = result['run_config']['direction-total']['orig']['rate_pps']
116                 actual = result['stats']['total_tx_rate']
117                 warning = self.traffic_client.compare_tx_rates(required, actual)
118                 if warning is not None:
119                     result['run_config']['warning'] = warning
120
121         traffic_result[frame_size].update(result)
122         return traffic_result
123
124     def __get_chain_result(self):
125         result = OrderedDict()
126         for fs in self.config.frame_sizes:
127             result.update(self.__get_result_per_frame_size(fs,
128                                                            self.config.traffic.bidirectional))
129         chain_result = {
130             'flow_count': self.config.flow_count,
131             'service_chain_count': self.config.service_chain_count,
132             'bidirectional': self.config.traffic.bidirectional,
133             'profile': self.config.traffic.profile,
134             'compute_nodes': self.stats_manager.get_compute_nodes_bios(),
135             'result': result
136         }
137         return chain_result
138
139     def run(self):
140         """Run the requested benchmark.
141
142         return: the results of the benchmark as a dict
143         """
144         results = {}
145         if self.config.no_traffic:
146             return results
147
148         LOG.info('Starting %dx%s benchmark...', self.config.service_chain_count, self.chain_name)
149         self.__setup_traffic()
150         # now that the dest MAC for all VNFs is known in all cases, it is time to create
151         # workers as they might be needed to extract stats prior to sending traffic
152         self.stats_manager.create_worker()
153
154         results[self.chain_name] = {'result': self.__get_chain_result()}
155
156         LOG.info("Service chain '%s' run completed.", self.chain_name)
157         return results
158
159     def close(self):
160         """Close this instance of chain runner and delete resources if applicable."""
161         try:
162             if not self.config.no_cleanup:
163                 LOG.info('Cleaning up...')
164                 if self.chain_manager:
165                     self.chain_manager.delete()
166             else:
167                 LOG.info('Clean up skipped.')
168             try:
169                 self.traffic_client.close()
170             except Exception:
171                 LOG.exception()
172             if self.stats_manager:
173                 self.stats_manager.close()
174         except Exception:
175             LOG.exception('Cleanup not finished')
176
177     def get_version(self):
178         """Retrieve the version of dependent components."""
179         versions = {}
180         if self.traffic_client:
181             versions['Traffic_Generator'] = self.traffic_client.get_version()
182         versions.update(self.stats_manager.get_version())
183         return versions