Merge "Update CLI Command in yardstick TC019, TC045~TC048"
[yardstick.git] / yardstick / benchmark / scenarios / networking / vnf_generic.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 """ NSPerf specific scenario definition """
15
16 from __future__ import absolute_import
17 import logging
18 import yaml
19
20 from yardstick.benchmark.scenarios import base
21 from yardstick.common.utils import import_modules_from_package, itersubclasses
22 from yardstick.network_services.collector.subscriber import Collector
23 from yardstick.network_services.vnf_generic import vnfdgen
24 from yardstick.network_services.vnf_generic.vnf.base import GenericVNF
25 from yardstick.network_services.traffic_profile.base import TrafficProfile
26 from yardstick import ssh
27
28 LOG = logging.getLogger(__name__)
29
30
31 class SSHError(Exception):
32     """Class handles ssh connection error exception"""
33     pass
34
35
36 class SSHTimeout(SSHError):
37     """Class handles ssh connection timeout exception"""
38     pass
39
40
41 class IncorrectConfig(Exception):
42     """Class handles incorrect configuration during setup"""
43     pass
44
45
46 class IncorrectSetup(Exception):
47     """Class handles incorrect setup during setup"""
48     pass
49
50
51 class SshManager(object):
52     def __init__(self, node):
53         super(SshManager, self).__init__()
54         self.node = node
55         self.conn = None
56
57     def __enter__(self):
58         """
59         args -> network device mappings
60         returns -> ssh connection ready to be used
61         """
62         try:
63             ssh_port = self.node.get("ssh_port", ssh.DEFAULT_PORT)
64             self.conn = ssh.SSH(user=self.node["user"],
65                                 host=self.node["ip"],
66                                 password=self.node["password"],
67                                 port=ssh_port)
68             self.conn.wait()
69         except (SSHError) as error:
70             LOG.info("connect failed to %s, due to %s", self.node["ip"], error)
71         # self.conn defaults to None
72         return self.conn
73
74     def __exit__(self, exc_type, exc_val, exc_tb):
75         if self.conn:
76             self.conn.close()
77
78
79 class NetworkServiceTestCase(base.Scenario):
80     """Class handles Generic framework to do pre-deployment VNF &
81        Network service testing  """
82
83     __scenario_type__ = "NSPerf"
84
85     def __init__(self, scenario_cfg, context_cfg):  # Yardstick API
86         super(NetworkServiceTestCase, self).__init__()
87         self.scenario_cfg = scenario_cfg
88         self.context_cfg = context_cfg
89
90         # fixme: create schema to validate all fields have been provided
91         with open(scenario_cfg["topology"]) as stream:
92             self.topology = yaml.load(stream)["nsd:nsd-catalog"]["nsd"][0]
93         self.vnfs = []
94         self.collector = None
95         self.traffic_profile = None
96
97     @classmethod
98     def _get_traffic_flow(cls, scenario_cfg):
99         try:
100             with open(scenario_cfg["traffic_options"]["flow"]) as fflow:
101                 flow = yaml.load(fflow)
102         except (KeyError, IOError, OSError):
103             flow = {}
104         return flow
105
106     @classmethod
107     def _get_traffic_imix(cls, scenario_cfg):
108         try:
109             with open(scenario_cfg["traffic_options"]["imix"]) as fimix:
110                 imix = yaml.load(fimix)
111         except (KeyError, IOError, OSError):
112             imix = {}
113         return imix
114
115     @classmethod
116     def _get_traffic_profile(cls, scenario_cfg, context_cfg):
117         traffic_profile_tpl = ""
118         private = {}
119         public = {}
120         try:
121             with open(scenario_cfg["traffic_profile"]) as infile:
122                 traffic_profile_tpl = infile.read()
123
124         except (KeyError, IOError, OSError):
125             raise
126
127         return [traffic_profile_tpl, private, public]
128
129     def _fill_traffic_profile(self, scenario_cfg, context_cfg):
130         traffic_profile = {}
131
132         flow = self._get_traffic_flow(scenario_cfg)
133
134         imix = self._get_traffic_imix(scenario_cfg)
135
136         traffic_mapping, private, public = \
137             self._get_traffic_profile(scenario_cfg, context_cfg)
138
139         traffic_profile = vnfdgen.generate_vnfd(traffic_mapping,
140                                                 {"imix": imix, "flow": flow,
141                                                  "private": private,
142                                                  "public": public})
143
144         return TrafficProfile.get(traffic_profile)
145
146     @classmethod
147     def _find_vnf_name_from_id(cls, topology, vnf_id):
148         return next((vnfd["vnfd-id-ref"]
149                      for vnfd in topology["constituent-vnfd"]
150                      if vnf_id == vnfd["member-vnf-index"]), None)
151
152     def _resolve_topology(self, context_cfg, topology):
153         for vld in topology["vld"]:
154             if len(vld["vnfd-connection-point-ref"]) > 2:
155                 raise IncorrectConfig("Topology file corrupted, "
156                                       "too many endpoint for connection")
157
158             node_0, node_1 = vld["vnfd-connection-point-ref"]
159
160             node0 = self._find_vnf_name_from_id(topology,
161                                                 node_0["member-vnf-index-ref"])
162             node1 = self._find_vnf_name_from_id(topology,
163                                                 node_1["member-vnf-index-ref"])
164
165             if0 = node_0["vnfd-connection-point-ref"]
166             if1 = node_1["vnfd-connection-point-ref"]
167
168             try:
169                 nodes = context_cfg["nodes"]
170                 nodes[node0]["interfaces"][if0]["vld_id"] = vld["id"]
171                 nodes[node1]["interfaces"][if1]["vld_id"] = vld["id"]
172
173                 nodes[node0]["interfaces"][if0]["dst_mac"] = \
174                     nodes[node1]["interfaces"][if1]["local_mac"]
175                 nodes[node0]["interfaces"][if0]["dst_ip"] = \
176                     nodes[node1]["interfaces"][if1]["local_ip"]
177
178                 nodes[node1]["interfaces"][if1]["dst_mac"] = \
179                     nodes[node0]["interfaces"][if0]["local_mac"]
180                 nodes[node1]["interfaces"][if1]["dst_ip"] = \
181                     nodes[node0]["interfaces"][if0]["local_ip"]
182             except KeyError:
183                 raise IncorrectConfig("Required interface not found,"
184                                       "topology file corrupted")
185
186     @classmethod
187     def _find_list_index_from_vnf_idx(cls, topology, vnf_idx):
188         return next((topology["constituent-vnfd"].index(vnfd)
189                      for vnfd in topology["constituent-vnfd"]
190                      if vnf_idx == vnfd["member-vnf-index"]), None)
191
192     def _update_context_with_topology(self, context_cfg, topology):
193         for idx in topology["constituent-vnfd"]:
194             vnf_idx = idx["member-vnf-index"]
195             nodes = context_cfg["nodes"]
196             node = self._find_vnf_name_from_id(topology, vnf_idx)
197             list_idx = self._find_list_index_from_vnf_idx(topology, vnf_idx)
198             nodes[node].update(topology["constituent-vnfd"][list_idx])
199
200     def map_topology_to_infrastructure(self, context_cfg, topology):
201         """ This method should verify if the available resources defined in pod.yaml
202         match the topology.yaml file.
203
204         :param topology:
205         :return: None. Side effect: context_cfg is updated
206         """
207
208         for node, node_dict in context_cfg["nodes"].items():
209
210             cmd = "PATH=$PATH:/sbin:/usr/sbin ip addr show"
211             with SshManager(node_dict) as conn:
212                 exit_status = conn.execute(cmd)[0]
213                 if exit_status != 0:
214                     raise IncorrectSetup("Node's %s lacks ip tool." % node)
215
216                 for interface in node_dict["interfaces"]:
217                     network = node_dict["interfaces"][interface]
218                     keys = ["vpci", "local_ip", "netmask",
219                             "local_mac", "driver", "dpdk_port_num"]
220                     missing = set(keys).difference(network)
221                     if missing:
222                         raise IncorrectConfig("Require interface fields '%s' "
223                                               "not found, topology file "
224                                               "corrupted" % ', '.join(missing))
225
226         # 3. Use topology file to find connections & resolve dest address
227         self._resolve_topology(context_cfg, topology)
228         self._update_context_with_topology(context_cfg, topology)
229
230     @classmethod
231     def get_vnf_impl(cls, vnf_model):
232         """ Find the implementing class from vnf_model["vnf"]["name"] field
233
234         :param vnf_model: dictionary containing a parsed vnfd
235         :return: subclass of GenericVNF
236         """
237         import_modules_from_package(
238             "yardstick.network_services.vnf_generic.vnf")
239         expected_name = vnf_model['id']
240         impl = (c for c in itersubclasses(GenericVNF)
241                 if c.__name__ == expected_name)
242         try:
243             return next(impl)
244         except StopIteration:
245             raise IncorrectConfig("No implementation for %s", expected_name)
246
247     def load_vnf_models(self, context_cfg):
248         """ Create VNF objects based on YAML descriptors
249
250         :param context_cfg:
251         :return:
252         """
253         vnfs = []
254         for node in context_cfg["nodes"]:
255             LOG.debug(context_cfg["nodes"][node])
256             with open(context_cfg["nodes"][node]["VNF model"]) as stream:
257                 vnf_model = stream.read()
258             vnfd = vnfdgen.generate_vnfd(vnf_model, context_cfg["nodes"][node])
259             vnf_impl = self.get_vnf_impl(vnfd["vnfd:vnfd-catalog"]["vnfd"][0])
260             vnf_instance = vnf_impl(vnfd["vnfd:vnfd-catalog"]["vnfd"][0])
261             vnf_instance.name = node
262             vnfs.append(vnf_instance)
263
264         return vnfs
265
266     def setup(self):
267         """ Setup infrastructure, provission VNFs & start traffic
268
269         :return:
270         """
271
272         # 1. Verify if infrastructure mapping can meet topology
273         self.map_topology_to_infrastructure(self.context_cfg, self.topology)
274         # 1a. Load VNF models
275         self.vnfs = self.load_vnf_models(self.context_cfg)
276         # 1b. Fill traffic profile with information from topology
277         self.traffic_profile = self._fill_traffic_profile(self.scenario_cfg,
278                                                           self.context_cfg)
279
280         # 2. Provision VNFs
281         try:
282             for vnf in self.vnfs:
283                 LOG.info("Instantiating %s", vnf.name)
284                 vnf.instantiate(self.scenario_cfg, self.context_cfg)
285         except RuntimeError:
286             for vnf in self.vnfs:
287                 vnf.terminate()
288             raise
289
290         # 3. Run experiment
291         # Start listeners first to avoid losing packets
292         traffic_runners = [vnf for vnf in self.vnfs if vnf.runs_traffic]
293         for traffic_gen in traffic_runners:
294             traffic_gen.listen_traffic(self.traffic_profile)
295
296         # register collector with yardstick for KPI collection.
297         self.collector = Collector(self.vnfs, self.traffic_profile)
298         self.collector.start()
299
300         # Start the actual traffic
301         for traffic_gen in traffic_runners:
302             LOG.info("Starting traffic on %s", traffic_gen.name)
303             traffic_gen.run_traffic(self.traffic_profile)
304
305     def run(self, result):  # yardstick API
306         """ Yardstick calls run() at intervals defined in the yaml and
307             produces timestamped samples
308
309         :param result: dictionary with results to update
310         :return: None
311         """
312
313         for vnf in self.vnfs:
314             # Result example:
315             # {"VNF1: { "tput" : [1000, 999] }, "VNF2": { "latency": 100 }}
316             LOG.debug("vnf")
317             result.update(self.collector.get_kpi(vnf))
318
319     def teardown(self):
320         """ Stop the collector and terminate VNF & TG instance
321
322         :return
323         """
324
325         self.collector.stop()
326         for vnf in self.vnfs:
327             LOG.info("Stopping %s", vnf.name)
328             vnf.terminate()