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