Added Landslide Resource Helper implementation
[yardstick.git] / yardstick / network_services / vnf_generic / vnf / tg_landslide.py
1 # Copyright (c) 2018 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 logging
16 import requests
17 import six
18 import time
19 from collections import Mapping
20
21 from yardstick.common import exceptions
22 from yardstick.common import utils as common_utils
23 from yardstick.network_services import utils as net_serv_utils
24 from yardstick.network_services.vnf_generic.vnf import sample_vnf
25
26 try:
27     from lsapi import LsApi
28 except ImportError:
29     LsApi = common_utils.ErrorClass
30
31 LOG = logging.getLogger(__name__)
32
33
34 class LandslideResourceHelper(sample_vnf.ClientResourceHelper):
35     """Landslide TG helper class"""
36
37     REST_STATUS_CODES = {'OK': 200, 'CREATED': 201, 'NO CHANGE': 409}
38     REST_API_CODES = {'NOT MODIFIED': 500810}
39
40     def __init__(self, setup_helper):
41         super(LandslideResourceHelper, self).__init__(setup_helper)
42         self._result = {}
43         self.vnfd_helper = setup_helper.vnfd_helper
44         self.scenario_helper = setup_helper.scenario_helper
45
46         # TAS Manager config initialization
47         self._url = None
48         self._user_id = None
49         self.session = None
50         self.license_data = {}
51
52         # TCL session initialization
53         self._tcl = LandslideTclClient(LsTclHandler(), self)
54
55         self.session = requests.Session()
56         self.running_tests_uri = 'runningTests'
57         self.test_session_uri = 'testSessions'
58         self.test_serv_uri = 'testServers'
59         self.suts_uri = 'suts'
60         self.users_uri = 'users'
61         self.user_lib_uri = None
62         self.run_id = None
63
64     def abort_running_tests(self, timeout=60, delay=5):
65         """ Abort running test sessions, if any """
66         _start_time = time.time()
67         while time.time() < _start_time + timeout:
68             run_tests_states = {x['id']: x['testStateOrStep']
69                                 for x in self.get_running_tests()}
70             if not set(run_tests_states.values()).difference(
71                     {'COMPLETE', 'COMPLETE_ERROR'}):
72                 break
73             else:
74                 [self.stop_running_tests(running_test_id=_id, force=True)
75                  for _id, _state in run_tests_states.items()
76                  if 'COMPLETE' not in _state]
77             time.sleep(delay)
78         else:
79             raise RuntimeError(
80                 'Some test runs not stopped during {} seconds'.format(timeout))
81
82     def _build_url(self, resource, action=None):
83         """ Build URL string
84
85         :param resource: REST API resource name
86         :type resource: str
87         :param action: actions name and value
88         :type action: dict('name': <str>, 'value': <str>)
89         :returns str: REST API resource name with optional action info
90         """
91         # Action is optional and accepted only in presence of resource param
92         if action and not resource:
93             raise ValueError("Resource name not provided")
94         # Concatenate actions
95         _action = ''.join(['?{}={}'.format(k, v) for k, v in
96                            action.items()]) if action else ''
97
98         return ''.join([self._url, resource, _action])
99
100     def get_response_params(self, method, resource, params=None):
101         """ Retrieve params from JSON response of specific resource URL
102
103         :param method: one of supported REST API methods
104         :type method: str
105         :param resource: URI, requested resource name
106         :type resource: str
107         :param params: attributes to be found in JSON response
108         :type params: list(str)
109         """
110         _res = []
111         params = params if params else []
112         response = self.exec_rest_request(method, resource)
113         # Get substring between last slash sign and question mark (if any)
114         url_last_part = resource.rsplit('/', 1)[-1].rsplit('?', 1)[0]
115         _response_json = response.json()
116         # Expect dict(), if URL last part and top dict key don't match
117         # Else, if they match, expect list()
118         k, v = list(_response_json.items())[0]
119         if k != url_last_part:
120             v = [v]  # v: list(dict(str: str))
121         # Extract params, or whole list of dicts (without top level key)
122         for x in v:
123             _res.append({param: x[param] for param in params} if params else x)
124         return _res
125
126     def _create_user(self, auth, level=1):
127         """ Create new user
128
129         :param auth: data to create user account on REST server
130         :type auth: dict
131         :param level: Landslide user permissions level
132         :type level: int
133         :returns int: user id
134         """
135         # Set expiration date in two years since account creation date
136         _exp_date = time.strftime(
137             '{}/%m/%d %H:%M %Z'.format(time.gmtime().tm_year + 2))
138         _username = auth['user']
139         _fields = {"contactInformation": "", "expiresOn": _exp_date,
140                    "fullName": "Test User",
141                    "isActive": "true", "level": level,
142                    "password": auth['password'],
143                    "username": _username}
144         _response = self.exec_rest_request('post', self.users_uri,
145                                            json_data=_fields, raise_exc=False)
146         _resp_json = _response.json()
147         if _response.status_code == self.REST_STATUS_CODES['CREATED']:
148             # New user created
149             _id = _resp_json['id']
150             LOG.info("New user created: username='%s', id='%s'", _username,
151                      _id)
152         elif _resp_json.get('apiCode') == self.REST_API_CODES['NOT MODIFIED']:
153             # User already exists
154             LOG.info("Account '%s' already exists.", _username)
155             # Get user id
156             _id = self._modify_user(_username, {"isActive": "true"})['id']
157         else:
158             raise exceptions.RestApiError(
159                 'Error during new user "{}" creation'.format(_username))
160         return _id
161
162     def _modify_user(self, username, fields):
163         """ Modify information about existing user
164
165         :param username: user name of account to be modified
166         :type username: str
167         :param fields: data to modify user account on REST server
168         :type fields: dict
169         :returns dict: user info
170         """
171         _response = self.exec_rest_request('post', self.users_uri,
172                                            action={'username': username},
173                                            json_data=fields, raise_exc=False)
174         if _response.status_code == self.REST_STATUS_CODES['OK']:
175             _response = _response.json()
176         else:
177             raise exceptions.RestApiError(
178                 'Error during user "{}" data update: {}'.format(
179                     username,
180                     _response.status_code))
181         LOG.info("User account '%s' modified: '%s'", username, _response)
182         return _response
183
184     def _delete_user(self, username):
185         """ Delete user account
186
187         :param username: username field
188         :type username: str
189         :returns bool: True if succeeded
190         """
191         self.exec_rest_request('delete', self.users_uri,
192                                action={'username': username})
193
194     def _get_users(self, username=None):
195         """ Get user records from REST server
196
197         :param username: username field
198         :type username: None|str
199         :returns list(dict): empty list, or user record, or list of all users
200         """
201         _response = self.get_response_params('get', self.users_uri)
202         _res = [u for u in _response if
203                 u['username'] == username] if username else _response
204         return _res
205
206     def exec_rest_request(self, method, resource, action=None, json_data=None,
207                           logs=True, raise_exc=True):
208         """ Execute REST API request, return response object
209
210         :param method: one of supported requests ('post', 'get', 'delete')
211         :type method: str
212         :param resource: URL of resource
213         :type resource: str
214         :param action: data used to provide URI located after question mark
215         :type action: dict
216         :param json_data: mandatory only for 'post' method
217         :type json_data: dict
218         :param logs: debug logs display flag
219         :type raise_exc: bool
220         :param raise_exc: if True, raise exception on REST API call error
221         :returns requests.Response(): REST API call response object
222         """
223         json_data = json_data if json_data else {}
224         action = action if action else {}
225         _method = method.upper()
226         method = method.lower()
227         if method not in ('post', 'get', 'delete'):
228             raise ValueError("Method '{}' not supported".format(_method))
229
230         if method == 'post' and not action:
231             if not (json_data and isinstance(json_data, Mapping)):
232                 raise ValueError(
233                     'JSON data missing in {} request'.format(_method))
234
235         r = getattr(self.session, method)(self._build_url(resource, action),
236                                           json=json_data)
237         if raise_exc and not r.ok:
238             msg = 'Failed to "{}" resource "{}". Reason: "{}"'.format(
239                 method, self._build_url(resource, action), r.reason)
240             raise exceptions.RestApiError(msg)
241
242         if logs:
243             LOG.debug("RC: %s | Request: %s | URL: %s", r.status_code, method,
244                       r.request.url)
245             LOG.debug("Response: %s", r.json())
246         return r
247
248     def connect(self):
249         """Connect to RESTful server using test user account"""
250         tas_info = self.vnfd_helper['mgmt-interface']
251         # Supported REST Server ports: HTTP - 8080, HTTPS - 8181
252         _port = '8080' if tas_info['proto'] == 'http' else '8181'
253         tas_info.update({'port': _port})
254         self._url = '{proto}://{ip}:{port}/api/'.format(**tas_info)
255         self.session.headers.update({'Accept': 'application/json',
256                                      'Content-type': 'application/json'})
257         # Login with super user to create test user
258         self.session.auth = (
259             tas_info['super-user'], tas_info['super-user-password'])
260         LOG.info("Connect using superuser: server='%s'", self._url)
261         auth = {x: tas_info[x] for x in ('user', 'password')}
262         self._user_id = self._create_user(auth)
263         # Login with test user
264         self.session.auth = auth['user'], auth['password']
265         # Test user validity
266         self.exec_rest_request('get', '')
267
268         self.user_lib_uri = 'libraries/{{}}/{}'.format(self.test_session_uri)
269         LOG.info("Login with test user: server='%s'", self._url)
270         # Read existing license
271         self.license_data['lic_id'] = tas_info['license']
272
273         # Tcl client init
274         self._tcl.connect(tas_info['ip'], *self.session.auth)
275
276         return self.session
277
278     def disconnect(self):
279         self.session = None
280         self._tcl.disconnect()
281
282     def terminate(self):
283         self._terminated.value = 1
284
285     def create_dmf(self, dmf):
286         if isinstance(dmf, list):
287             for _dmf in dmf:
288                 self._tcl.create_dmf(_dmf)
289         else:
290             self._tcl.create_dmf(dmf)
291
292     def delete_dmf(self, dmf):
293         if isinstance(dmf, list):
294             for _dmf in dmf:
295                 self._tcl.delete_dmf(_dmf)
296         else:
297             self._tcl.delete_dmf(dmf)
298
299     def create_suts(self, suts):
300         # Keep only supported keys in suts object
301         for _sut in suts:
302             sut_entry = {k: v for k, v in _sut.items()
303                          if k not in {'phy', 'nextHop', 'role'}}
304             _response = self.exec_rest_request(
305                 'post', self.suts_uri, json_data=sut_entry,
306                 logs=False, raise_exc=False)
307             if _response.status_code != self.REST_STATUS_CODES['CREATED']:
308                 LOG.info(_response.reason)  # Failed to create
309                 _name = sut_entry.pop('name')
310                 # Modify existing SUT
311                 self.configure_sut(sut_name=_name, json_data=sut_entry)
312             else:
313                 LOG.info("SUT created: %s", sut_entry)
314
315     def get_suts(self, suts_id=None):
316         if suts_id:
317             _suts = self.exec_rest_request(
318                 'get', '{}/{}'.format(self.suts_uri, suts_id)).json()
319         else:
320             _suts = self.get_response_params('get', self.suts_uri)
321
322         return _suts
323
324     def configure_sut(self, sut_name, json_data):
325         """ Modify information of specific SUTs
326
327         :param sut_name: name of existing SUT
328         :type sut_name: str
329         :param json_data: SUT settings
330         :type json_data: dict()
331         """
332         LOG.info("Modifying SUT information...")
333         _response = self.exec_rest_request('post',
334                                            self.suts_uri,
335                                            action={'name': sut_name},
336                                            json_data=json_data,
337                                            raise_exc=False)
338         if _response.status_code not in {self.REST_STATUS_CODES[x] for x in
339                                          {'OK', 'NO CHANGE'}}:
340             raise exceptions.RestApiError(_response.reason)
341
342         LOG.info("Modified SUT: %s", sut_name)
343
344     def delete_suts(self, suts_ids=None):
345         if not suts_ids:
346             _curr_suts = self.get_response_params('get', self.suts_uri)
347             suts_ids = [x['id'] for x in _curr_suts]
348         LOG.info("Deleting SUTs with following IDs: %s", suts_ids)
349         for _id in suts_ids:
350             self.exec_rest_request('delete',
351                                    '{}/{}'.format(self.suts_uri, _id))
352             LOG.info("\tDone for SUT id: %s", _id)
353
354     def _check_test_servers_state(self, test_servers_ids=None, delay=10,
355                                   timeout=300):
356         LOG.info("Waiting for related test servers state change to READY...")
357         # Wait on state change
358         _start_time = time.time()
359         while time.time() - _start_time < timeout:
360             ts_ids_not_ready = {x['id'] for x in
361                                 self.get_test_servers(test_servers_ids)
362                                 if x['state'] != 'READY'}
363             if ts_ids_not_ready == set():
364                 break
365             time.sleep(delay)
366         else:
367             raise RuntimeError(
368                 'Test servers not in READY state after {} seconds.'.format(
369                     timeout))
370
371     def create_test_servers(self, test_servers):
372         """ Create test servers
373
374         :param test_servers: input data for test servers creation
375                              mandatory fields: managementIp
376                              optional fields: name
377         :type test_servers: list(dict)
378         """
379         _ts_ids = []
380         for _ts in test_servers:
381             _msg = 'Created test server "%(name)s"'
382             _ts_ids.append(self._tcl.create_test_server(_ts))
383             if _ts.get('thread_model'):
384                 _msg += ' in mode: "%(thread_model)s"'
385                 LOG.info(_msg, _ts)
386
387         self._check_test_servers_state(_ts_ids)
388
389     def get_test_servers(self, test_server_ids=None):
390         if not test_server_ids:  # Get all test servers info
391             _test_servers = self.exec_rest_request(
392                 'get', self.test_serv_uri).json()[self.test_serv_uri]
393             LOG.info("Current test servers configuration: %s", _test_servers)
394             return _test_servers
395
396         _test_servers = []
397         for _id in test_server_ids:
398             _test_servers.append(self.exec_rest_request(
399                 'get', '{}/{}'.format(self.test_serv_uri, _id)).json())
400         LOG.info("Current test servers configuration: %s", _test_servers)
401         return _test_servers
402
403     def configure_test_servers(self, action, json_data=None,
404                                test_server_ids=None):
405         if not test_server_ids:
406             test_server_ids = [x['id'] for x in self.get_test_servers()]
407         elif isinstance(test_server_ids, int):
408             test_server_ids = [test_server_ids]
409         for _id in test_server_ids:
410             self.exec_rest_request('post',
411                                    '{}/{}'.format(self.test_serv_uri, _id),
412                                    action=action, json_data=json_data)
413             LOG.info("Test server (id: %s) configuration done: %s", _id,
414                      action)
415         return test_server_ids
416
417     def delete_test_servers(self, test_servers_ids=None):
418         # Delete test servers
419         for _ts in self.get_test_servers(test_servers_ids):
420             self.exec_rest_request('delete', '{}/{}'.format(self.test_serv_uri,
421                                                             _ts['id']))
422             LOG.info("Deleted test server: %s", _ts['name'])
423
424     def create_test_session(self, test_session):
425         # Use tcl client to create session
426         test_session['library'] = self._user_id
427         LOG.debug("Creating session='%s'", test_session['name'])
428         self._tcl.create_test_session(test_session)
429
430     def get_test_session(self, test_session_name=None):
431         if test_session_name:
432             uri = 'libraries/{}/{}/{}'.format(self._user_id,
433                                               self.test_session_uri,
434                                               test_session_name)
435         else:
436             uri = self.user_lib_uri.format(self._user_id)
437         _test_sessions = self.exec_rest_request('get', uri).json()
438         return _test_sessions
439
440     def configure_test_session(self, template_name, test_session):
441         # Override specified test session parameters
442         LOG.info('Update test session parameters: %s', test_session['name'])
443         test_session.update({'library': self._user_id})
444         return self.exec_rest_request(
445             method='post',
446             action={'action': 'overrideAndSaveAs'},
447             json_data=test_session,
448             resource='{}/{}'.format(self.user_lib_uri.format(self._user_id),
449                                     template_name))
450
451     def delete_test_session(self, test_session):
452         return self.exec_rest_request('delete', '{}/{}'.format(
453             self.user_lib_uri.format(self._user_id), test_session))
454
455     def create_running_tests(self, test_session_name):
456         r = self.exec_rest_request('post',
457                                    self.running_tests_uri,
458                                    json_data={'library': self._user_id,
459                                               'name': test_session_name})
460         if r.status_code != self.REST_STATUS_CODES['CREATED']:
461             raise exceptions.RestApiError('Failed to start test session.')
462         self.run_id = r.json()['id']
463
464     def get_running_tests(self, running_test_id=None):
465         """Get JSON structure of specified running test entity
466
467         :param running_test_id: ID of created running test entity
468         :type running_test_id: int
469         :returns list: running tests entity
470         """
471         if not running_test_id:
472             running_test_id = ''
473         _res_name = '{}/{}'.format(self.running_tests_uri, running_test_id)
474         _res = self.exec_rest_request('get', _res_name, logs=False).json()
475         # If no run_id specified, skip top level key in response dict.
476         # Else return JSON as list
477         return _res.get('runningTests', [_res])
478
479     def delete_running_tests(self, running_test_id=None):
480         if not running_test_id:
481             running_test_id = ''
482         _res_name = '{}/{}'.format(self.running_tests_uri, running_test_id)
483         self.get_response_params('delete', _res_name)
484         LOG.info("Deleted running test with id: %s", running_test_id)
485
486     def _running_tests_action(self, running_test_id, action, json_data=None):
487         if not json_data:
488             json_data = {}
489         # Supported actions:
490         # 'stop', 'abort', 'continue', 'update', 'sendTcCommand', 'sendOdc'
491         _res_name = '{}/{}'.format(self.running_tests_uri, running_test_id)
492         self.exec_rest_request('post', _res_name, {'action': action},
493                                json_data)
494         LOG.debug("Executed action: '%s' on running test id: %s", action,
495                   running_test_id)
496
497     def stop_running_tests(self, running_test_id, json_data=None, force=False):
498         _action = 'abort' if force else 'stop'
499         self._running_tests_action(running_test_id, _action,
500                                    json_data=json_data)
501         LOG.info('Performed action: "%s" to test run with id: %s', _action,
502                  running_test_id)
503
504     def check_running_test_state(self, run_id):
505         r = self.exec_rest_request('get',
506                                    '{}/{}'.format(self.running_tests_uri,
507                                                   run_id))
508         return r.json().get("testStateOrStep")
509
510     def get_running_tests_results(self, run_id):
511         _res = self.exec_rest_request(
512             'get',
513             '{}/{}/{}'.format(self.running_tests_uri,
514                               run_id,
515                               'measurements')).json()
516         return _res
517
518     def _write_results(self, results):
519         # Avoid None value at test session start
520         _elapsed_time = results['elapsedTime'] if results['elapsedTime'] else 0
521
522         _res_tabs = results.get('tabs')
523         # Avoid parsing 'tab' dict key initially (missing or empty)
524         if not _res_tabs:
525             return
526
527         # Flatten nested dict holding Landslide KPIs of current test run
528         flat_kpis_dict = {}
529         for _tab, _kpis in six.iteritems(_res_tabs):
530             for _kpi, _value in six.iteritems(_kpis):
531                 # Combine table name and KPI name using delimiter "::"
532                 _key = '::'.join([_tab, _kpi])
533                 try:
534                     # Cast value from str to float
535                     # Remove comma and/or measure units, e.g. "us"
536                     flat_kpis_dict[_key] = float(
537                         _value.split(' ')[0].replace(',', ''))
538                 except ValueError:  # E.g. if KPI represents datetime
539                     pass
540         LOG.info("Polling test results of test run id: %s. Elapsed time: %s "
541                  "seconds", self.run_id, _elapsed_time)
542         return flat_kpis_dict
543
544     def collect_kpi(self):
545         if 'COMPLETE' in self.check_running_test_state(self.run_id):
546             self._result.update({'done': True})
547             return self._result
548         _res = self.get_running_tests_results(self.run_id)
549         _kpis = self._write_results(_res)
550         if _kpis:
551             _kpis.update({'run_id': int(self.run_id)})
552             _kpis.update({'iteration': _res['iteration']})
553             self._result.update(_kpis)
554             return self._result
555
556
557 class LandslideTclClient(object):
558     """Landslide TG TCL client class"""
559
560     DEFAULT_TEST_NODE = {
561         'ethStatsEnabled': True,
562         'forcedEthInterface': '',
563         'innerVlanId': 0,
564         'ip': '',
565         'mac': '',
566         'mtu': 1500,
567         'nextHop': '',
568         'numLinksOrNodes': 1,
569         'numVlan': 1,
570         'phy': '',
571         'uniqueVlanAddr': False,
572         'vlanDynamic': 0,
573         'vlanId': 0,
574         'vlanUserPriority': 0,
575         'vlanTagType': 0
576     }
577
578     TEST_NODE_CMD = \
579         'ls::create -TestNode-{} -under $p_ -Type "eth"' \
580         ' -Phy "{phy}" -Ip "{ip}" -NumLinksOrNodes {numLinksOrNodes}' \
581         ' -NextHop "{nextHop}" -Mac "{mac}" -MTU {mtu} ' \
582         ' -ForcedEthInterface "{forcedEthInterface}"' \
583         ' -EthStatsEnabled {ethStatsEnabled}' \
584         ' -VlanId {vlanId} -VlanUserPriority {vlanUserPriority}' \
585         ' -NumVlan {numVlan} -UniqueVlanAddr {uniqueVlanAddr}' \
586         ';'
587
588     def __init__(self, tcl_handler, ts_context):
589         self.tcl_server_ip = None
590         self._user = None
591         self._library_id = None
592         self._basic_library_id = None
593         self._tcl = tcl_handler
594         self._ts_context = ts_context
595         self.ts_ids = set()
596
597         # Test types names expected in session profile, test case and pod files
598         self._tc_types = {"SGW_Nodal", "SGW_Node", "MME_Nodal", "PGW_Node",
599                           "PCRF_Node"}
600
601         self._class_param_config_handler = {
602             "Array": self._configure_array_param,
603             "TestNode": self._configure_test_node_param,
604             "Sut": self._configure_sut_param,
605             "Dmf": self._configure_dmf_param
606         }
607
608     def connect(self, tcl_server_ip, username, password):
609         """ Connect to TCL server with username and password
610
611         :param tcl_server_ip: TCL server IP address
612         :type tcl_server_ip: str
613         :param username: existing username on TCL server
614         :type username: str
615         :param password: password related to username on TCL server
616         :type password: str
617         """
618         LOG.info("connect: server='%s' user='%s'", tcl_server_ip, username)
619         res = self._tcl.execute(
620             "ls::login {} {} {}".format(tcl_server_ip, username, password))
621         if 'java0x' not in res:  # handle assignment reflects login success
622             raise exceptions.LandslideTclException(
623                 "connect: login failed ='{}'.".format(res))
624         self._library_id = self._tcl.execute(
625             "ls::get [ls::query LibraryInfo -userLibraryName {}] -Id".format(
626                 username))
627         self._basic_library_id = self._get_library_id('Basic')
628         self.tcl_server_ip = tcl_server_ip
629         self._user = username
630         LOG.debug("connect: user='%s' me='%s' basic='%s'", self._user,
631                   self._library_id,
632                   self._basic_library_id)
633
634     def disconnect(self):
635         """ Disconnect from TCL server. Drop TCL connection configuration """
636         LOG.info("disconnect: server='%s' user='%s'",
637                  self.tcl_server_ip, self._user)
638         self._tcl.execute("ls::logout")
639         self.tcl_server_ip = None
640         self._user = None
641         self._library_id = None
642         self._basic_library_id = None
643
644     def _add_test_server(self, name, ip):
645         try:
646             # Check if test server exists with name equal to _ts_name
647             ts_id = int(self.resolve_test_server_name(name))
648         except ValueError:
649             # Such test server does not exist. Attempt to create it
650             ts_id = self._tcl.execute(
651                 'ls::perform AddTs -Name "{}" -Ip "{}"'.format(name, ip))
652             try:
653                 int(ts_id)
654             except ValueError:
655                 # Failed to create test server, e.g. limit reached
656                 raise RuntimeError(
657                     'Failed to create test server: "{}". {}'.format(name,
658                                                                     ts_id))
659         return ts_id
660
661     def _update_license(self, name):
662         """ Setup/update test server license
663
664         :param name: test server name
665         :type name: str
666         """
667         # Retrieve current TsInfo configuration, result stored in handle "ts"
668         self._tcl.execute(
669             'set ts [ls::retrieve TsInfo -Name "{}"]'.format(name))
670
671         # Set license ID, if it differs from current one, update test server
672         _curr_lic_id = self._tcl.execute('ls::get $ts -RequestedLicense')
673         if _curr_lic_id != self._ts_context.license_data['lic_id']:
674             self._tcl.execute('ls::config $ts -RequestedLicense {}'.format(
675                 self._ts_context.license_data['lic_id']))
676             self._tcl.execute('ls::perform ModifyTs $ts')
677
678     def _set_thread_model(self, name, thread_model):
679         # Retrieve test server configuration, store it in handle "tsc"
680         _cfguser_password = self._ts_context.vnfd_helper['mgmt-interface'][
681             'cfguser_password']
682         self._tcl.execute(
683             'set tsc [ls::perform RetrieveTsConfiguration '
684             '-name "{}" {}]'.format(name, _cfguser_password))
685         # Configure ThreadModel, if it differs from current one
686         thread_model_map = {'Legacy': 'V0',
687                             'Max': 'V1',
688                             'Fireball': 'V1_FB3'}
689         _model = thread_model_map[thread_model]
690         _curr_model = self._tcl.execute('ls::get $tsc -ThreadModel')
691         if _curr_model != _model:
692             self._tcl.execute(
693                 'ls::config $tsc -ThreadModel "{}"'.format(_model))
694             self._tcl.execute(
695                 'ls::perform ApplyTsConfiguration $tsc {}'.format(
696                     _cfguser_password))
697
698     def create_test_server(self, test_server):
699         _ts_thread_model = test_server.get('thread_model')
700         _ts_name = test_server['name']
701
702         ts_id = self._add_test_server(_ts_name, test_server['ip'])
703
704         self._update_license(_ts_name)
705
706         # Skip below code modifying thread_model if it is not defined
707         if _ts_thread_model:
708             self._set_thread_model(_ts_name, _ts_thread_model)
709
710         return ts_id
711
712     def create_test_session(self, test_session):
713         """ Create, configure and save Landslide test session object.
714
715         :param test_session: Landslide TestSession object
716         :type test_session: dict
717         """
718         LOG.info("create_test_session: name='%s'", test_session['name'])
719         self._tcl.execute('set test_ [ls::create TestSession]')
720         self._tcl.execute('ls::config $test_ -Library {} -Name "{}"'.format(
721                 self._library_id, test_session['name']))
722         self._tcl.execute('ls::config $test_ -Description "{}"'.format(
723             test_session['description']))
724         if 'keywords' in test_session:
725             self._tcl.execute('ls::config $test_ -Keywords "{}"'.format(
726                 test_session['keywords']))
727         if 'duration' in test_session:
728             self._tcl.execute('ls::config $test_ -Duration "{}"'.format(
729                 test_session['duration']))
730         if 'iterations' in test_session:
731             self._tcl.execute('ls::config $test_ -Iterations "{}"'.format(
732                 test_session['iterations']))
733         if 'reservePorts' in test_session:
734             if test_session['reservePorts']:
735                 self._tcl.execute('ls::config $test_ -Reserve Ports')
736
737         if 'reservations' in test_session:
738             for _reservation in test_session['reservations']:
739                 self._configure_reservation(_reservation)
740
741         if 'reportOptions' in test_session:
742             self._configure_report_options(test_session['reportOptions'])
743
744         for _index, _group in enumerate(test_session['tsGroups']):
745             self._configure_ts_group(_group, _index)
746
747         self._save_test_session()
748
749     def create_dmf(self, dmf):
750         """ Create, configure and save Landslide Data Message Flow object.
751
752         :param dmf: Landslide Data Message Flow object
753         :type: dmf: dict
754         """
755         self._tcl.execute('set dmf_ [ls::create Dmf]')
756         _lib_id = self._get_library_id(dmf['dmf']['library'])
757         self._tcl.execute('ls::config $dmf_ -Library {} -Name "{}"'.format(
758             _lib_id,
759             dmf['dmf']['name']))
760         for _param_key in dmf:
761             if _param_key == 'dmf':
762                 continue
763             _param_value = dmf[_param_key]
764             if isinstance(_param_value, dict):
765                 # Configure complex parameter
766                 _tcl_cmd = 'ls::config $dmf_'
767                 for _sub_param_key in _param_value:
768                     _sub_param_value = _param_value[_sub_param_key]
769                     if isinstance(_sub_param_value, str):
770                         _tcl_cmd += ' -{} "{}"'.format(_sub_param_key,
771                                                        _sub_param_value)
772                     else:
773                         _tcl_cmd += ' -{} {}'.format(_sub_param_key,
774                                                      _sub_param_value)
775
776                 self._tcl.execute(_tcl_cmd)
777             else:
778                 # Configure simple parameter
779                 if isinstance(_param_value, str):
780                     self._tcl.execute(
781                         'ls::config $dmf_ -{} "{}"'.format(_param_key,
782                                                            _param_value))
783                 else:
784                     self._tcl.execute(
785                         'ls::config $dmf_ -{} {}'.format(_param_key,
786                                                          _param_value))
787         self._save_dmf()
788
789     def configure_dmf(self, dmf):
790         # Use create to reconfigure and overwrite existing dmf
791         self.create_dmf(dmf)
792
793     def delete_dmf(self, dmf):
794         raise NotImplementedError
795
796     def _save_dmf(self):
797         # Call 'Validate' to set default values for missing parameters
798         res = self._tcl.execute('ls::perform Validate -Dmf $dmf_')
799         if res == 'Invalid':
800             res = self._tcl.execute('ls::get $dmf_ -ErrorsAndWarnings')
801             LOG.error("_save_dmf: %s", res)
802             raise exceptions.LandslideTclException("_save_dmf: {}".format(res))
803         else:
804             res = self._tcl.execute('ls::save $dmf_ -overwrite')
805             LOG.debug("_save_dmf: result (%s)", res)
806
807     def _configure_report_options(self, options):
808         for _option_key in options:
809             _option_value = options[_option_key]
810             if _option_key == 'format':
811                 _format = 0
812                 if _option_value == 'CSV':
813                     _format = 1
814                 self._tcl.execute(
815                     'ls::config $test_.ReportOptions -Format {} '
816                     '-Ts -3 -Tc -3'.format(_format))
817             else:
818                 self._tcl.execute(
819                     'ls::config $test_.ReportOptions -{} {}'.format(
820                         _option_key,
821                         _option_value))
822
823     def _configure_ts_group(self, ts_group, ts_group_index):
824         try:
825             _ts_id = int(self.resolve_test_server_name(ts_group['tsId']))
826         except ValueError:
827             raise RuntimeError('Test server name "{}" does not exist.'.format(
828                 ts_group['tsId']))
829         if _ts_id not in self.ts_ids:
830             self._tcl.execute(
831                 'set tss_ [ls::create TsGroup -under $test_ -tsId {} ]'.format(
832                     _ts_id))
833             self.ts_ids.add(_ts_id)
834         for _case in ts_group.get('testCases', []):
835             self._configure_tc_type(_case, ts_group_index)
836
837         self._configure_preresolved_arp(ts_group.get('preResolvedArpAddress'))
838
839     def _configure_tc_type(self, tc, ts_group_index):
840         if tc['type'] not in self._tc_types:
841             raise RuntimeError('Test type {} not supported.'.format(
842                 tc['type']))
843         tc['type'] = tc['type'].replace('_', ' ')
844         res = self._tcl.execute(
845             'set tc_ [ls::retrieve testcase -libraryId {0} "{1}"]'.format(
846                 self._basic_library_id, tc['type']))
847         if 'Invalid' in res:
848             raise RuntimeError('Test type {} not found in "Basic" '
849                                'library.'.format(tc['type']))
850         self._tcl.execute(
851             'ls::config $test_.TsGroup({}) -children-Tc $tc_'.format(
852                 ts_group_index))
853         self._tcl.execute('ls::config $tc_ -Library {0} -Name "{1}"'.format(
854             self._basic_library_id, tc['name']))
855         self._tcl.execute(
856             'ls::config $tc_ -Description "{}"'.format(tc['type']))
857         self._tcl.execute(
858             'ls::config $tc_ -Keywords "GTP LTE {}"'.format(tc['type']))
859         if 'linked' in tc:
860             self._tcl.execute(
861                 'ls::config $tc_ -Linked {}'.format(tc['linked']))
862         if 'AssociatedPhys' in tc:
863             self._tcl.execute('ls::config $tc_ -AssociatedPhys "{}"'.format(
864                 tc['AssociatedPhys']))
865         if 'parameters' in tc:
866             self._configure_parameters(tc['parameters'])
867
868     def _configure_parameters(self, params):
869         self._tcl.execute('set p_ [ls::get $tc_ -children-Parameters(0)]')
870         for _param_key in sorted(params):
871             _param_value = params[_param_key]
872             if isinstance(_param_value, dict):
873                 # Configure complex parameter
874                 if _param_value['class'] in self._class_param_config_handler:
875                     self._class_param_config_handler[_param_value['class']](
876                         _param_key,
877                         _param_value)
878             else:
879                 # Configure simple parameter
880                 self._tcl.execute(
881                     'ls::create {} -under $p_ -Value "{}"'.format(
882                         _param_key,
883                         _param_value))
884
885     def _configure_array_param(self, name, params):
886         self._tcl.execute('ls::create -Array-{} -under $p_ ;'.format(name))
887         for param in params['array']:
888             self._tcl.execute(
889                 'ls::create ArrayItem -under $p_.{} -Value "{}"'.format(name,
890                                                                         param))
891
892     def _configure_test_node_param(self, name, params):
893         _params = self.DEFAULT_TEST_NODE
894         _params.update(params)
895
896         # TCL command expects lower case 'true' or 'false'
897         _params['ethStatsEnabled'] = str(_params['ethStatsEnabled']).lower()
898         _params['uniqueVlanAddr'] = str(_params['uniqueVlanAddr']).lower()
899
900         cmd = self.TEST_NODE_CMD.format(name, **_params)
901         self._tcl.execute(cmd)
902
903     def _configure_sut_param(self, name, params):
904         self._tcl.execute(
905             'ls::create -Sut-{} -under $p_ -Name "{}";'.format(name,
906                                                                params['name']))
907
908     def _configure_dmf_param(self, name, params):
909         self._tcl.execute('ls::create -Dmf-{} -under $p_ ;'.format(name))
910
911         for _flow_index, _flow in enumerate(params['mainflows']):
912             _lib_id = self._get_library_id(_flow['library'])
913             self._tcl.execute(
914                 'ls::perform AddDmfMainflow $p_.Dmf {} "{}"'.format(
915                     _lib_id,
916                     _flow['name']))
917
918             if not params.get('instanceGroups'):
919                 return
920
921             _instance_group = params['instanceGroups'][_flow_index]
922
923             # Traffic Mixer parameters handling
924             for _key in ['mixType', 'rate']:
925                 if _key in _instance_group:
926                     self._tcl.execute(
927                         'ls::config $p_.Dmf.InstanceGroup({}) -{} {}'.format(
928                             _flow_index, _key, _instance_group[_key]))
929
930             # Assignments parameters handling
931             for _row_id, _row in enumerate(_instance_group.get('rows', [])):
932                 self._tcl.execute(
933                     'ls::config $p_.Dmf.InstanceGroup({}).Row({}) -Node {} '
934                     '-OverridePort {} -ClientPort {} -Context {} -Role {} '
935                     '-PreferredTransport {} -RatingGroup {} '
936                     '-ServiceID {}'.format(
937                         _flow_index, _row_id, _row['node'],
938                         _row['overridePort'], _row['clientPort'],
939                         _row['context'], _row['role'], _row['transport'],
940                         _row['ratingGroup'], _row['serviceId']))
941
942     def _configure_reservation(self, reservation):
943         _ts_id = self.resolve_test_server_name(reservation['tsId'])
944         self._tcl.execute(
945             'set reservation_ [ls::create Reservation -under $test_]')
946         self._tcl.execute(
947             'ls::config $reservation_ -TsIndex {} -TsId {} '
948             '-TsName "{}"'.format(reservation['tsIndex'],
949                                   _ts_id,
950                                   reservation['tsName']))
951         for _subnet in reservation['phySubnets']:
952             self._tcl.execute(
953                 'set physubnet_ [ls::create PhySubnet -under $reservation_]')
954             self._tcl.execute(
955                 'ls::config $physubnet_ -Name "{}" -Base "{}" -Mask "{}" '
956                 '-NumIps {}'.format(_subnet['name'], _subnet['base'],
957                                     _subnet['mask'], _subnet['numIps']))
958
959     def _configure_preresolved_arp(self, pre_resolved_arp):
960         if not pre_resolved_arp:  # Pre-resolved ARP configuration not found
961             return
962         for _entry in pre_resolved_arp:
963             # TsGroup handle name should correspond in _configure_ts_group()
964             self._tcl.execute(
965                 'ls::create PreResolvedArpAddress -under $tss_ '
966                 '-StartingAddress "{StartingAddress}" '
967                 '-NumNodes {NumNodes}'.format(**_entry))
968
969     def delete_test_session(self, test_session):
970         raise NotImplementedError
971
972     def _save_test_session(self):
973         # Call 'Validate' to set default values for missing parameters
974         res = self._tcl.execute('ls::perform Validate -TestSession $test_')
975         if res == 'Invalid':
976             res = self._tcl.execute('ls::get $test_ -ErrorsAndWarnings')
977             raise exceptions.LandslideTclException(
978                 "Test session validation failed. Server response: {}".format(
979                     res))
980         else:
981             self._tcl.execute('ls::save $test_ -overwrite')
982             LOG.debug("Test session saved successfully.")
983
984     def _get_library_id(self, library):
985         _library_id = self._tcl.execute(
986             "ls::get [ls::query LibraryInfo -systemLibraryName {}] -Id".format(
987                 library))
988         try:
989             int(_library_id)
990             return _library_id
991         except ValueError:
992             pass
993
994         _library_id = self._tcl.execute(
995             "ls::get [ls::query LibraryInfo -userLibraryName {}] -Id".format(
996                 library))
997         try:
998             int(_library_id)
999         except ValueError:
1000             LOG.error("_get_library_id: library='%s' not found.", library)
1001             raise exceptions.LandslideTclException(
1002                 "_get_library_id: library='{}' not found.".format(
1003                     library))
1004
1005         return _library_id
1006
1007     def resolve_test_server_name(self, ts_name):
1008         return self._tcl.execute("ls::query TsId {}".format(ts_name))
1009
1010
1011 class LsTclHandler(object):
1012     """Landslide TCL Handler class"""
1013
1014     LS_OK = "ls_ok"
1015     JRE_PATH = net_serv_utils.get_nsb_option('jre_path_i386')
1016
1017     def __init__(self):
1018         self.tcl_cmds = {}
1019         self._ls = LsApi(jre_path=self.JRE_PATH)
1020         self._ls.tcl(
1021             "ls::config ApiOptions -NoReturnSuccessResponseString '{}'".format(
1022                 self.LS_OK))
1023
1024     def execute(self, command):
1025         res = self._ls.tcl(command)
1026         self.tcl_cmds[command] = res
1027         return res