Merge "Handle HTTP traffic profile parameters as optional"
[yardstick.git] / yardstick / network_services / traffic_profile / http_ixload.py
1 # Copyright (c) 2016-2017 Intel Corporation
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain 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,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 import sys
16 import os
17 import logging
18 import collections
19 import subprocess
20 try:
21     libs = subprocess.check_output(
22         'python -c "import site; print(site.getsitepackages())"', shell=True)
23
24     sys.path.extend(libs[1:-1].replace("'", "").split(','))
25 except subprocess.CalledProcessError:
26     pass
27
28 # ixload uses its own py2. So importing jsonutils fails. So adding below
29 # workaround to support call from yardstick
30 try:
31     from oslo_serialization import jsonutils
32 except ImportError:
33     import json as jsonutils
34
35 from yardstick.common import exceptions #pylint: disable=wrong-import-position
36
37 try:
38     from IxLoad import IxLoad, StatCollectorUtils
39 except ImportError:
40     IxLoad = exceptions.ErrorClass
41     StatCollectorUtils = exceptions.ErrorClass
42
43
44 LOG = logging.getLogger(__name__)
45 CSV_FILEPATH_NAME = 'IxL_statResults.csv'
46
47 STATS_TO_GET = (
48     'HTTP_Client.csv',
49     'HTTP_Server.csv',
50     'L2-3 Stats for Client Ports.csv',
51     'L2-3 Stats for Server Ports.csv',
52     'IxLoad Detailed Report.html',
53     'IxLoad Detailed Report.pdf'
54 )
55
56 HTTP_CLIENT_STATS = [
57     ["HTTP Client", "TCP Connections Established", "kSum"],
58     ["HTTP Client", "TCP Connection Requests Failed", "kSum"],
59     ["HTTP Client", "HTTP Simulated Users", "kSum"],
60     ["HTTP Client", "HTTP Concurrent Connections", "kSum"],
61     ["HTTP Client", "HTTP Connections", "kSum"],
62     ["HTTP Client", "HTTP Transactions", "kSum"],
63     ["HTTP Client", "HTTP Connection Attempts", "kSum"]
64 ]
65
66 HTTP_SERVER_STATS = [
67     ["HTTP Server", "TCP Connections Established", "kSum"],
68     ["HTTP Server", "TCP Connection Requests Failed", "kSum"]
69 ]
70
71
72 INCOMING_STAT_RECORD_TEMPLATE = """
73 =====================================
74 INCOMING STAT RECORD >>> %s
75 Len = %s
76 %s
77 %s
78 =====================================
79 """
80
81 INCOMING_STAT_INTERVAL_TEMPLATE = """
82 =====================================
83 Incoming stats: Time interval: %s
84 Incoming stats: Time interval: %s
85 =====================================
86 """
87
88
89 def validate_non_string_sequence(value, default=None, raise_exc=None):
90     if isinstance(value, collections.Sequence) and not isinstance(value, str):
91         return value
92     if raise_exc:
93         raise raise_exc  # pylint: disable=raising-bad-type
94     return default
95
96
97 def join_non_strings(separator, *non_strings):
98     try:
99         non_strings = validate_non_string_sequence(non_strings[0], raise_exc=RuntimeError)
100     except (IndexError, RuntimeError):
101         pass
102     return str(separator).join(str(non_string) for non_string in non_strings)
103
104
105 class IXLOADHttpTest(object):
106
107     def __init__(self, test_input):
108         self.ix_load = None
109         self.stat_utils = None
110         self.remote_server = None
111         self.config_file = None
112         self.results_on_windows = None
113         self.result_dir = None
114         self.chassis = None
115         self.card = None
116         self.ports_to_reassign = None
117         self.links_param = None
118         self.test_input = jsonutils.loads(test_input)
119         self.parse_run_test()
120         self.test = None
121
122     @staticmethod
123     def format_ports_for_reassignment(ports):
124         formatted = [join_non_strings(';', p) for p in ports if len(p) == 3]
125         LOG.debug('for client ports:%s', os.linesep.join(formatted))
126         return formatted
127
128     def reassign_ports(self, test, repository, ports_to_reassign):
129         LOG.debug('ReassignPorts: %s %s', test, repository)
130
131         chassis_chain = repository.cget('chassisChain')
132         LOG.debug('chassischain: %s', chassis_chain)
133         client_ports = ports_to_reassign[0::2]
134         server_ports = ports_to_reassign[1::2]
135
136         client_ports = self.format_ports_for_reassignment(client_ports)
137         LOG.debug('Reassigning client ports: %s', client_ports)
138         server_ports = self.format_ports_for_reassignment(server_ports)
139         LOG.debug('Reassigning server ports: %s', server_ports)
140         ports_to_set = client_ports + server_ports
141
142         try:
143             LOG.debug('Reassigning ports: %s', ports_to_set)
144             test.setPorts(ports_to_set)
145         except Exception:
146             LOG.error('Error: Could not remap port assignment for: %s',
147                       ports_to_set)
148             self.ix_load.delete(repository)
149             self.ix_load.disconnect()
150             raise
151
152     @staticmethod
153     def stat_collector(*args):
154         LOG.debug(INCOMING_STAT_RECORD_TEMPLATE, args, len(args), args[0], args[1])
155
156     @staticmethod
157     def IxL_StatCollectorCommand(*args):
158         stats = args[1][3]
159         timestamp = args[1][1]
160         LOG.debug(INCOMING_STAT_INTERVAL_TEMPLATE, timestamp, stats)
161
162     @staticmethod
163     def set_results_dir(test_controller, results_on_windows):
164         """
165         If the folder doesn't exists on the Windows Client PC,
166         IxLoad will automatically create it.
167         """
168         try:
169             test_controller.setResultDir(results_on_windows)
170         except Exception:
171             LOG.error('Error creating results dir on Win: %s',
172                       results_on_windows)
173             raise
174
175     def load_config_file(self, config_file):
176         try:
177             LOG.debug(config_file)
178             repository = self.ix_load.new("ixRepository", name=config_file)
179             return repository
180         except Exception:
181             LOG.error('Error: IxLoad config file not found: %s', config_file)
182             raise
183
184     def update_network_address(self, net_traffic, address, gateway, prefix):
185         """Update ip address and gateway for net_traffic object
186
187         This function update field which configure source addresses for
188         traffic which is described by net_traffic object.
189         Do not return anything
190
191         :param net_traffic: (IxLoadObjectProxy) proxy obj to tcl net_traffic object
192         :param address: (str) Ipv4 range start address
193         :param gateway: (str) Ipv4 address of gateway
194         :param prefix: (int) subnet prefix
195         :return:
196         """
197         try:
198             ethernet = net_traffic.network.getL1Plugin()
199             ix_net_l2_ethernet_plugin = ethernet.childrenList[0]
200             ix_net_ip_v4_v6_plugin = ix_net_l2_ethernet_plugin.childrenList[0]
201             ix_net_ip_v4_v6_range = ix_net_ip_v4_v6_plugin.rangeList[0]
202
203             ix_net_ip_v4_v6_range.config(
204                 prefix=prefix,
205                 ipAddress=address,
206                 gatewayAddress=gateway)
207         except Exception:
208             raise exceptions.InvalidRxfFile
209
210     def update_network_mac_address(self, net_traffic, mac):
211         """Update MACaddress for net_traffic object
212
213         This function update field which configure MACaddresses for
214         traffic which is described by net_traffic object.
215         If mac == "auto" then will be configured auto generated mac
216         Do not return anything.
217
218         :param net_traffic: (IxLoadObjectProxy) proxy obj to tcl net_traffic object
219         :param mac: (str) MAC
220         :return:
221         """
222         try:
223             ethernet = net_traffic.network.getL1Plugin()
224             ix_net_l2_ethernet_plugin = ethernet.childrenList[0]
225             ix_net_ip_v4_v6_plugin = ix_net_l2_ethernet_plugin.childrenList[0]
226             ix_net_ip_v4_v6_range = ix_net_ip_v4_v6_plugin.rangeList[0]
227
228             if str(mac).lower() == "auto":
229                 ix_net_ip_v4_v6_range.config(autoMacGeneration=True)
230             else:
231                 ix_net_ip_v4_v6_range.config(autoMacGeneration=False)
232                 mac_range = ix_net_ip_v4_v6_range.getLowerRelatedRange(
233                     "MacRange")
234                 mac_range.config(mac=mac)
235         except Exception:
236             raise exceptions.InvalidRxfFile
237
238     def update_network_param(self, net_traffic, param):
239         """Update net_traffic by parameters specified in param"""
240
241         self.update_network_address(net_traffic, param["address"],
242                                     param["gateway"], param["subnet_prefix"])
243
244         self.update_network_mac_address(net_traffic, param["mac"])
245
246     def update_config(self):
247         """Update some fields by parameters from traffic profile"""
248
249         net_traffics = {}
250         # self.test.communityList is a IxLoadObjectProxy to some tcl object
251         # which contain all net_traffic objects in scenario.
252         # net_traffic item has a name in format "activity_name@item_name"
253         try:
254             for item in self.test.communityList:
255                 net_traffics[item.name.split('@')[1]] = item
256         except Exception:  # pylint: disable=broad-except
257             pass
258
259         for name, net_traffic in net_traffics.items():
260             try:
261                 param = self.links_param[name]
262             except KeyError:
263                 LOG.debug('There is no param for net_traffic %s', name)
264                 continue
265
266             self.update_network_param(net_traffic, param["ip"])
267             if "uplink" in name:
268                 self.update_http_client_param(net_traffic, param["http_client"])
269
270     def update_http_client_param(self, net_traffic, param):
271         """Update http client object in net_traffic
272
273         Update http client object in net_traffic by parameters
274         specified in param.
275         Do not return anything.
276
277         :param net_traffic: (IxLoadObjectProxy) proxy obj to tcl net_traffic object
278         :param param: (dict) http_client section from traffic profile
279         :return:
280         """
281         page = param.get("page_object")
282         if page:
283             self.update_page_size(net_traffic, page)
284         users = param.get("simulated_users")
285         if users:
286             self.update_user_count(net_traffic, users)
287
288     def update_page_size(self, net_traffic, page_object):
289         """Update page_object field in http client object in net_traffic
290
291         This function update field which configure page_object
292         which will be loaded from server
293         Do not return anything.
294
295         :param net_traffic: (IxLoadObjectProxy) proxy obj to tcl net_traffic object
296         :param page_object: (str) path to object on server e.g. "/4k.html"
297         :return:
298         """
299         try:
300             activity = net_traffic.activityList[0]
301             ix_http_command = activity.agent.actionList[0]
302             ix_http_command.config(pageObject=page_object)
303         except Exception:
304             raise exceptions.InvalidRxfFile
305
306     def update_user_count(self, net_traffic, user_count):
307         """Update userObjectiveValue field in activity object in net_traffic
308
309         This function update field which configure users count
310         which will be simulated by client.
311         Do not return anything.
312
313         :param net_traffic: (IxLoadObjectProxy) proxy obj to tcl net_traffic object
314         :param user_count: (int) number of simulated users
315         :return:
316         """
317         try:
318             activity = net_traffic.activityList[0]
319             activity.config(userObjectiveValue=user_count)
320         except Exception:
321             raise exceptions.InvalidRxfFile
322
323     def start_http_test(self):
324         self.ix_load = IxLoad()
325
326         LOG.debug('--- ixLoad obj: %s', self.ix_load)
327         try:
328             self.ix_load.connect(self.remote_server)
329         except Exception:
330             raise
331
332         log_tag = "IxLoad-api"
333         log_name = "reprun"
334         logger = self.ix_load.new("ixLogger", log_tag, 1)
335         log_engine = logger.getEngine()
336         log_engine.setLevels(self.ix_load.ixLogger.kLevelDebug,
337                              self.ix_load.ixLogger.kLevelInfo)
338         log_engine.setFile(log_name, 2, 256, 1)
339
340         # Initialize stat collection utilities
341         self.stat_utils = StatCollectorUtils()
342
343         test_controller = self.ix_load.new("ixTestController", outputDir=1)
344
345         repository = self.load_config_file(self.config_file)
346
347         # Get the first test on the testList
348         test_name = repository.testList[0].cget("name")
349         self.test = repository.testList.getItem(test_name)
350
351         self.set_results_dir(test_controller, self.results_on_windows)
352
353         self.test.config(statsRequired=1, enableResetPorts=1, csvInterval=2,
354                          enableForceOwnership=True)
355
356         self.update_config()
357
358         #  ---- Remap ports ----
359         try:
360             self.reassign_ports(self.test, repository, self.ports_to_reassign)
361         except Exception:  # pylint: disable=broad-except
362             LOG.exception("Exception occurred during reassign_ports")
363
364         # -----------------------------------------------------------------------
365         # Set up stat Collection
366         # -----------------------------------------------------------------------
367         test_server_handle = test_controller.getTestServerHandle()
368         self.stat_utils.Initialize(test_server_handle)
369
370         # Clear any stats that may have been registered previously
371         self.stat_utils.ClearStats()
372
373         # Define the stats we would like to collect
374         self.stat_utils.AddStat(caption="Watch_Stat_1",
375                                 statSourceType="HTTP Client",
376                                 statName="TCP Connections Established",
377                                 aggregationType="kSum",
378                                 filterList={})
379
380         self.stat_utils.AddStat(caption="Watch_Stat_2",
381                                 statSourceType="HTTP Client",
382                                 statName="TCP Connection Requests Failed",
383                                 aggregationType="kSum",
384                                 filterList={})
385
386         self.stat_utils.AddStat(caption="Watch_Stat_3",
387                                 statSourceType="HTTP Server",
388                                 statName="TCP Connections Established",
389                                 aggregationType="kSum",
390                                 filterList={})
391
392         self.stat_utils.AddStat(caption="Watch_Stat_4",
393                                 statSourceType="HTTP Server",
394                                 statName="TCP Connection Requests Failed",
395                                 aggregationType="kSum",
396                                 filterList={})
397
398         self.stat_utils.StartCollector(self.IxL_StatCollectorCommand)
399
400         test_controller.run(self.test)
401         self.ix_load.waitForTestFinish()
402
403         test_controller.releaseConfigWaitFinish()
404
405         # Stop the collector (running in the tcl event loop)
406         self.stat_utils.StopCollector()
407
408         # Cleanup
409         test_controller.generateReport(detailedReport=1, format="PDF;HTML")
410         test_controller.releaseConfigWaitFinish()
411
412         self.ix_load.delete(self.test)
413         self.ix_load.delete(test_controller)
414         self.ix_load.delete(logger)
415         self.ix_load.delete(log_engine)
416
417         LOG.debug('Retrieving CSV stats from Windows Client PC ...')
418         for stat_file in STATS_TO_GET:
419             enhanced_stat_file = stat_file.replace('-', '')
420             enhanced_stat_file = enhanced_stat_file.replace(' ', '_')
421             enhanced_stat_file = enhanced_stat_file.replace('__', '_')
422
423             LOG.debug('Getting csv stat file: %s', stat_file)
424             src_file = os.path.join(self.results_on_windows, stat_file)
425             dst_file = os.path.join(self.result_dir, '_'.join(['ixLoad', enhanced_stat_file]))
426             self.ix_load.retrieveFileCopy(src_file, dst_file)
427
428         self.ix_load.disconnect()
429
430     def parse_run_test(self):
431         self.remote_server = self.test_input["remote_server"]
432         LOG.debug("remote tcl server: %s", self.remote_server)
433
434         self.config_file = self.test_input["ixload_cfg"]
435         LOG.debug("ixload config: %s", self.remote_server)
436
437         self.results_on_windows = 'C:/Results'
438         self.result_dir = self.test_input["result_dir"]
439         self.chassis = self.test_input["ixia_chassis"]
440         LOG.debug("remote ixia chassis: %s", self.chassis)
441
442         self.card = self.test_input["IXIA"]["card"]
443         self.ports_to_reassign = [
444             [self.chassis, self.card, port] for port in
445             self.test_input["IXIA"]["ports"]
446         ]
447
448         LOG.debug("Ports to be reassigned: %s", self.ports_to_reassign)
449
450         self.links_param = self.test_input["links_param"]
451         LOG.debug("Links param to be applied: %s", self.links_param)
452
453
454 def main(args):
455     # Get the args from cmdline and parse and run the test
456     test_input = "".join(args[1:])
457     if test_input:
458         ixload_obj = IXLOADHttpTest(test_input)
459         ixload_obj.start_http_test()
460
461 if __name__ == '__main__':
462     LOG.info("Start http_ixload test")
463     main(sys.argv)