NFVBENCH-107 NFVbench 2.0 ValueError at end of fixed rate run
[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, actual_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(actual_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                         if frame_size != actual_frame_size:
109                             results[dr]['l2frame_size'] = frame_size
110                             results[dr]['actual_l2frame_size'] = actual_frame_size
111                         traffic_result[frame_size][dr] = results[dr]
112                         if 'warning' in results[dr]['stats'] and results[dr]['stats']['warning']:
113                             traffic_result['warning'] = results[dr]['stats']['warning']
114                 traffic_result[frame_size]['iteration_stats'] = results['iteration_stats']
115
116             if self.config.single_run:
117                 result['run_config'] = self.traffic_client.get_run_config(result)
118                 required = result['run_config']['direction-total']['orig']['rate_pps']
119                 actual = result['stats']['total_tx_rate']
120                 if frame_size != actual_frame_size:
121                     result['actual_l2frame_size'] = actual_frame_size
122                 warning = self.traffic_client.compare_tx_rates(required, actual)
123                 if warning is not None:
124                     result['run_config']['warning'] = warning
125
126         traffic_result[frame_size].update(result)
127         return traffic_result
128
129     def __get_chain_result(self):
130         result = OrderedDict()
131         for fs, actual_fs in zip(self.config.frame_sizes, self.config.actual_frame_sizes):
132             result.update(self.__get_result_per_frame_size(fs,
133                                                            actual_fs,
134                                                            self.config.traffic.bidirectional))
135         chain_result = {
136             'flow_count': self.config.flow_count,
137             'service_chain_count': self.config.service_chain_count,
138             'bidirectional': self.config.traffic.bidirectional,
139             'profile': self.config.traffic.profile,
140             'compute_nodes': self.stats_manager.get_compute_nodes_bios(),
141             'result': result
142         }
143         return chain_result
144
145     def run(self):
146         """Run the requested benchmark.
147
148         return: the results of the benchmark as a dict
149         """
150         results = {}
151         if self.config.no_traffic:
152             return results
153
154         LOG.info('Starting %dx%s benchmark...', self.config.service_chain_count, self.chain_name)
155         self.__setup_traffic()
156         # now that the dest MAC for all VNFs is known in all cases, it is time to create
157         # workers as they might be needed to extract stats prior to sending traffic
158         self.stats_manager.create_worker()
159
160         results[self.chain_name] = {'result': self.__get_chain_result()}
161
162         LOG.info("Service chain '%s' run completed.", self.chain_name)
163         return results
164
165     def close(self):
166         """Close this instance of chain runner and delete resources if applicable."""
167         try:
168             if not self.config.no_cleanup:
169                 LOG.info('Cleaning up...')
170                 if self.chain_manager:
171                     self.chain_manager.delete()
172             else:
173                 LOG.info('Clean up skipped.')
174             try:
175                 self.traffic_client.close()
176             except Exception:
177                 LOG.exception()
178             if self.stats_manager:
179                 self.stats_manager.close()
180         except Exception:
181             LOG.exception('Cleanup not finished')
182
183     def get_version(self):
184         """Retrieve the version of dependent components."""
185         versions = {}
186         if self.traffic_client:
187             versions['Traffic_Generator'] = self.traffic_client.get_version()
188         versions.update(self.stats_manager.get_version())
189         return versions