Report: Remove VNF
[csit.git] / resources / tools / presentation / specification_parser.py
1 # Copyright (c) 2019 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """Specification
15
16 Parsing of the specification YAML file.
17 """
18
19
20 import logging
21 from yaml import load, YAMLError
22 from pprint import pformat
23
24 from errors import PresentationError
25 from utils import get_last_successful_build_number
26 from utils import get_last_completed_build_number
27
28
29 class Specification(object):
30     """Specification of Presentation and analytics layer.
31
32     - based on specification specified in the specification YAML file
33     - presentation and analytics layer is model driven
34     """
35
36     # Tags are used in specification YAML file and replaced while the file is
37     # parsed.
38     TAG_OPENER = "{"
39     TAG_CLOSER = "}"
40
41     def __init__(self, cfg_file):
42         """Initialization.
43
44         :param cfg_file: File handler for the specification YAML file.
45         :type cfg_file: BinaryIO
46         """
47         self._cfg_file = cfg_file
48         self._cfg_yaml = None
49
50         self._specification = {"environment": dict(),
51                                "configuration": dict(),
52                                "static": dict(),
53                                "input": dict(),
54                                "output": dict(),
55                                "tables": list(),
56                                "plots": list(),
57                                "files": list(),
58                                "cpta": dict()}
59
60     @property
61     def specification(self):
62         """Getter - specification.
63
64         :returns: Specification.
65         :rtype: dict
66         """
67         return self._specification
68
69     @property
70     def environment(self):
71         """Getter - environment.
72
73         :returns: Environment specification.
74         :rtype: dict
75         """
76         return self._specification["environment"]
77
78     @property
79     def configuration(self):
80         """Getter - configuration.
81
82         :returns: Configuration of PAL.
83         :rtype: dict
84         """
85         return self._specification["configuration"]
86
87     @property
88     def static(self):
89         """Getter - static content.
90
91         :returns: Static content specification.
92         :rtype: dict
93         """
94         return self._specification["static"]
95
96     @property
97     def mapping(self):
98         """Getter - Mapping.
99
100         :returns: Mapping of the old names of test cases to the new (actual)
101             one.
102         :rtype: dict
103         """
104         return self._specification["configuration"]["mapping"]
105
106     @property
107     def ignore(self):
108         """Getter - Ignore list.
109
110         :returns: List of ignored test cases.
111         :rtype: list
112         """
113         return self._specification["configuration"]["ignore"]
114
115     @property
116     def alerting(self):
117         """Getter - Alerting.
118
119         :returns: Specification of alerts.
120         :rtype: dict
121         """
122         return self._specification["configuration"]["alerting"]
123
124     @property
125     def input(self):
126         """Getter - specification - inputs.
127         - jobs and builds.
128
129         :returns: Inputs.
130         :rtype: dict
131         """
132         return self._specification["input"]
133
134     @property
135     def builds(self):
136         """Getter - builds defined in specification.
137
138         :returns: Builds defined in the specification.
139         :rtype: dict
140         """
141         return self.input["builds"]
142
143     @property
144     def output(self):
145         """Getter - specification - output formats and versions to be generated.
146         - formats: html, pdf
147         - versions: full, ...
148
149         :returns: Outputs to be generated.
150         :rtype: dict
151         """
152         return self._specification["output"]
153
154     @property
155     def tables(self):
156         """Getter - tables to be generated.
157
158         :returns: List of specifications of tables to be generated.
159         :rtype: list
160         """
161         return self._specification["tables"]
162
163     @property
164     def plots(self):
165         """Getter - plots to be generated.
166
167         :returns: List of specifications of plots to be generated.
168         :rtype: list
169         """
170         return self._specification["plots"]
171
172     @property
173     def files(self):
174         """Getter - files to be generated.
175
176         :returns: List of specifications of files to be generated.
177         :rtype: list
178         """
179         return self._specification["files"]
180
181     @property
182     def cpta(self):
183         """Getter - Continuous Performance Trending and Analysis to be
184         generated.
185
186         :returns: List of specifications of Continuous Performance Trending and
187         Analysis to be generated.
188         :rtype: list
189         """
190         return self._specification["cpta"]
191
192     def set_input_state(self, job, build_nr, state):
193         """Set the state of input
194
195         :param job:
196         :param build_nr:
197         :param state:
198         :return:
199         """
200
201         try:
202             for build in self._specification["input"]["builds"][job]:
203                 if build["build"] == build_nr:
204                     build["status"] = state
205                     break
206             else:
207                 raise PresentationError("Build '{}' is not defined for job '{}'"
208                                         " in specification file.".
209                                         format(build_nr, job))
210         except KeyError:
211             raise PresentationError("Job '{}' and build '{}' is not defined in "
212                                     "specification file.".format(job, build_nr))
213
214     def set_input_file_name(self, job, build_nr, file_name):
215         """Set the state of input
216
217         :param job:
218         :param build_nr:
219         :param file_name:
220         :return:
221         """
222
223         try:
224             for build in self._specification["input"]["builds"][job]:
225                 if build["build"] == build_nr:
226                     build["file-name"] = file_name
227                     break
228             else:
229                 raise PresentationError("Build '{}' is not defined for job '{}'"
230                                         " in specification file.".
231                                         format(build_nr, job))
232         except KeyError:
233             raise PresentationError("Job '{}' and build '{}' is not defined in "
234                                     "specification file.".format(job, build_nr))
235
236     def _get_build_number(self, job, build_type):
237         """Get the number of the job defined by its name:
238          - lastSuccessfulBuild
239          - lastCompletedBuild
240
241         :param job: Job name.
242         :param build_type: Build type:
243          - lastSuccessfulBuild
244          - lastCompletedBuild
245         :type job" str
246         :raises PresentationError: If it is not possible to get the build
247         number.
248         :returns: The build number.
249         :rtype: int
250         """
251
252         # defined as a range <start, end>
253         if build_type == "lastSuccessfulBuild":
254             # defined as a range <start, lastSuccessfulBuild>
255             ret_code, build_nr, _ = get_last_successful_build_number(
256                 self.environment["urls"]["URL[JENKINS,CSIT]"], job)
257         elif build_type == "lastCompletedBuild":
258             # defined as a range <start, lastCompletedBuild>
259             ret_code, build_nr, _ = get_last_completed_build_number(
260                 self.environment["urls"]["URL[JENKINS,CSIT]"], job)
261         else:
262             raise PresentationError("Not supported build type: '{0}'".
263                                     format(build_type))
264         if ret_code != 0:
265             raise PresentationError("Not possible to get the number of the "
266                                     "build number.")
267         try:
268             build_nr = int(build_nr)
269             return build_nr
270         except ValueError as err:
271             raise PresentationError("Not possible to get the number of the "
272                                     "build number.\nReason: {0}".format(err))
273
274     def _get_type_index(self, item_type):
275         """Get index of item type (environment, input, output, ...) in
276         specification YAML file.
277
278         :param item_type: Item type: Top level items in specification YAML file,
279         e.g.: environment, input, output.
280         :type item_type: str
281         :returns: Index of the given item type.
282         :rtype: int
283         """
284
285         index = 0
286         for item in self._cfg_yaml:
287             if item["type"] == item_type:
288                 return index
289             index += 1
290         return None
291
292     def _find_tag(self, text):
293         """Find the first tag in the given text. The tag is enclosed by the
294         TAG_OPENER and TAG_CLOSER.
295
296         :param text: Text to be searched.
297         :type text: str
298         :returns: The tag, or None if not found.
299         :rtype: str
300         """
301         try:
302             start = text.index(self.TAG_OPENER)
303             end = text.index(self.TAG_CLOSER, start + 1) + 1
304             return text[start:end]
305         except ValueError:
306             return None
307
308     def _replace_tags(self, data, src_data=None):
309         """Replace tag(s) in the data by their values.
310
311         :param data: The data where the tags will be replaced by their values.
312         :param src_data: Data where the tags are defined. It is dictionary where
313         the key is the tag and the value is the tag value. If not given, 'data'
314         is used instead.
315         :type data: str or dict
316         :type src_data: dict
317         :returns: Data with the tags replaced.
318         :rtype: str or dict
319         :raises: PresentationError if it is not possible to replace the tag or
320         the data is not the supported data type (str, dict).
321         """
322
323         if src_data is None:
324             src_data = data
325
326         if isinstance(data, str):
327             tag = self._find_tag(data)
328             if tag is not None:
329                 data = data.replace(tag, src_data[tag[1:-1]])
330
331         elif isinstance(data, dict):
332             counter = 0
333             for key, value in data.items():
334                 tag = self._find_tag(value)
335                 if tag is not None:
336                     try:
337                         data[key] = value.replace(tag, src_data[tag[1:-1]])
338                         counter += 1
339                     except KeyError:
340                         raise PresentationError("Not possible to replace the "
341                                                 "tag '{}'".format(tag))
342             if counter:
343                 self._replace_tags(data, src_data)
344         else:
345             raise PresentationError("Replace tags: Not supported data type.")
346
347         return data
348
349     def _parse_env(self):
350         """Parse environment specification in the specification YAML file.
351         """
352
353         logging.info("Parsing specification file: environment ...")
354
355         idx = self._get_type_index("environment")
356         if idx is None:
357             return None
358
359         try:
360             self._specification["environment"]["configuration"] = \
361                 self._cfg_yaml[idx]["configuration"]
362         except KeyError:
363             self._specification["environment"]["configuration"] = None
364
365         try:
366             self._specification["environment"]["paths"] = \
367                 self._replace_tags(self._cfg_yaml[idx]["paths"])
368         except KeyError:
369             self._specification["environment"]["paths"] = None
370
371         try:
372             self._specification["environment"]["urls"] = \
373                 self._cfg_yaml[idx]["urls"]
374         except KeyError:
375             self._specification["environment"]["urls"] = None
376
377         try:
378             self._specification["environment"]["make-dirs"] = \
379                 self._cfg_yaml[idx]["make-dirs"]
380         except KeyError:
381             self._specification["environment"]["make-dirs"] = None
382
383         try:
384             self._specification["environment"]["remove-dirs"] = \
385                 self._cfg_yaml[idx]["remove-dirs"]
386         except KeyError:
387             self._specification["environment"]["remove-dirs"] = None
388
389         try:
390             self._specification["environment"]["build-dirs"] = \
391                 self._cfg_yaml[idx]["build-dirs"]
392         except KeyError:
393             self._specification["environment"]["build-dirs"] = None
394
395         try:
396             self._specification["environment"]["testbeds"] = \
397                 self._cfg_yaml[idx]["testbeds"]
398         except KeyError:
399             self._specification["environment"]["testbeds"] = None
400
401         logging.info("Done.")
402
403     def _parse_configuration(self):
404         """Parse configuration of PAL in the specification YAML file.
405         """
406
407         logging.info("Parsing specification file: configuration ...")
408
409         idx = self._get_type_index("configuration")
410         if idx is None:
411             logging.warning("No configuration information in the specification "
412                             "file.")
413             return None
414
415         try:
416             self._specification["configuration"] = self._cfg_yaml[idx]
417
418         except KeyError:
419             raise PresentationError("No configuration defined.")
420
421         # Data sets: Replace ranges by lists
422         for set_name, data_set in self.configuration["data-sets"].items():
423             if not isinstance(data_set, dict):
424                 continue
425             for job, builds in data_set.items():
426                 if builds:
427                     if isinstance(builds, dict):
428                         build_nr = builds.get("end", None)
429                         try:
430                             build_nr = int(build_nr)
431                         except ValueError:
432                             # defined as a range <start, build_type>
433                             build_nr = self._get_build_number(job, build_nr)
434                         builds = [x for x in range(builds["start"], build_nr+1)]
435                         self.configuration["data-sets"][set_name][job] = builds
436
437         # Data sets: add sub-sets to sets (only one level):
438         for set_name, data_set in self.configuration["data-sets"].items():
439             if isinstance(data_set, list):
440                 new_set = dict()
441                 for item in data_set:
442                     try:
443                         for key, val in self.configuration["data-sets"][item].\
444                                 items():
445                             new_set[key] = val
446                     except KeyError:
447                         raise PresentationError(
448                             "Data set {0} is not defined in "
449                             "the configuration section.".format(item))
450                 self.configuration["data-sets"][set_name] = new_set
451
452         # Mapping table:
453         mapping = None
454         mapping_file_name = self._specification["configuration"].\
455             get("mapping-file", None)
456         if mapping_file_name:
457             logging.debug("Mapping file: '{0}'".format(mapping_file_name))
458             try:
459                 with open(mapping_file_name, 'r') as mfile:
460                     mapping = load(mfile)
461                 logging.debug("Loaded mapping table:\n{0}".format(mapping))
462             except (YAMLError, IOError) as err:
463                 raise PresentationError(
464                     msg="An error occurred while parsing the mapping file "
465                         "'{0}'.".format(mapping_file_name),
466                     details=repr(err))
467         # Make sure everything is lowercase
468         if mapping:
469             self._specification["configuration"]["mapping"] = \
470                 {key.lower(): val.lower() for key, val in mapping.iteritems()}
471         else:
472             self._specification["configuration"]["mapping"] = dict()
473
474         # Ignore list:
475         ignore = None
476         ignore_list_name = self._specification["configuration"].\
477             get("ignore-list", None)
478         if ignore_list_name:
479             logging.debug("Ignore list file: '{0}'".format(ignore_list_name))
480             try:
481                 with open(ignore_list_name, 'r') as ifile:
482                     ignore = load(ifile)
483                 logging.debug("Loaded ignore list:\n{0}".format(ignore))
484             except (YAMLError, IOError) as err:
485                 raise PresentationError(
486                     msg="An error occurred while parsing the ignore list file "
487                         "'{0}'.".format(ignore_list_name),
488                     details=repr(err))
489         # Make sure everything is lowercase
490         if ignore:
491             self._specification["configuration"]["ignore"] = \
492                 [item.lower() for item in ignore]
493         else:
494             self._specification["configuration"]["ignore"] = list()
495
496         logging.info("Done.")
497
498     def _parse_input(self):
499         """Parse input specification in the specification YAML file.
500
501         :raises: PresentationError if there are no data to process.
502         """
503
504         logging.info("Parsing specification file: input ...")
505
506         idx = self._get_type_index("input")
507         if idx is None:
508             raise PresentationError("No data to process.")
509
510         try:
511             for key, value in self._cfg_yaml[idx]["general"].items():
512                 self._specification["input"][key] = value
513             self._specification["input"]["builds"] = dict()
514
515             for job, builds in self._cfg_yaml[idx]["builds"].items():
516                 if builds:
517                     if isinstance(builds, dict):
518                         build_nr = builds.get("end", None)
519                         try:
520                             build_nr = int(build_nr)
521                         except ValueError:
522                             # defined as a range <start, build_type>
523                             build_nr = self._get_build_number(job, build_nr)
524                         builds = [x for x in range(builds["start"], build_nr+1)]
525                     self._specification["input"]["builds"][job] = list()
526                     for build in builds:
527                         self._specification["input"]["builds"][job]. \
528                             append({"build": build, "status": None})
529
530                 else:
531                     logging.warning("No build is defined for the job '{}'. "
532                                     "Trying to continue without it.".
533                                     format(job))
534         except KeyError:
535             raise PresentationError("No data to process.")
536
537         logging.info("Done.")
538
539     def _parse_output(self):
540         """Parse output specification in the specification YAML file.
541
542         :raises: PresentationError if there is no output defined.
543         """
544
545         logging.info("Parsing specification file: output ...")
546
547         idx = self._get_type_index("output")
548         if idx is None:
549             raise PresentationError("No output defined.")
550
551         try:
552             self._specification["output"] = self._cfg_yaml[idx]
553         except (KeyError, IndexError):
554             raise PresentationError("No output defined.")
555
556         logging.info("Done.")
557
558     def _parse_static(self):
559         """Parse specification of the static content in the specification YAML
560         file.
561         """
562
563         logging.info("Parsing specification file: static content ...")
564
565         idx = self._get_type_index("static")
566         if idx is None:
567             logging.warning("No static content specified.")
568
569         for key, value in self._cfg_yaml[idx].items():
570             if isinstance(value, str):
571                 try:
572                     self._cfg_yaml[idx][key] = self._replace_tags(
573                         value, self._specification["environment"]["paths"])
574                 except KeyError:
575                     pass
576
577         self._specification["static"] = self._cfg_yaml[idx]
578
579         logging.info("Done.")
580
581     def _parse_elements(self):
582         """Parse elements (tables, plots) specification in the specification
583         YAML file.
584         """
585
586         logging.info("Parsing specification file: elements ...")
587
588         count = 1
589         for element in self._cfg_yaml:
590             try:
591                 element["output-file"] = self._replace_tags(
592                     element["output-file"],
593                     self._specification["environment"]["paths"])
594             except KeyError:
595                 pass
596
597             try:
598                 element["input-file"] = self._replace_tags(
599                     element["input-file"],
600                     self._specification["environment"]["paths"])
601             except KeyError:
602                 pass
603
604             # add data sets to the elements:
605             if isinstance(element.get("data", None), str):
606                 data_set = element["data"]
607                 try:
608                     element["data"] = self.configuration["data-sets"][data_set]
609                 except KeyError:
610                     raise PresentationError("Data set {0} is not defined in "
611                                             "the configuration section.".
612                                             format(data_set))
613
614             if element["type"] == "table":
615                 logging.info("  {:3d} Processing a table ...".format(count))
616                 try:
617                     element["template"] = self._replace_tags(
618                         element["template"],
619                         self._specification["environment"]["paths"])
620                 except KeyError:
621                     pass
622
623                 # add data sets
624                 try:
625                     for item in ("reference", "compare"):
626                         if element.get(item, None):
627                             data_set = element[item].get("data", None)
628                             if isinstance(data_set, str):
629                                 element[item]["data"] = \
630                                     self.configuration["data-sets"][data_set]
631
632                     if element.get("history", None):
633                         for i in range(len(element["history"])):
634                             data_set = element["history"][i].get("data", None)
635                             if isinstance(data_set, str):
636                                 element["history"][i]["data"] = \
637                                     self.configuration["data-sets"][data_set]
638
639                 except KeyError:
640                     raise PresentationError("Wrong data set used in {0}.".
641                                             format(element.get("title", "")))
642
643                 self._specification["tables"].append(element)
644                 count += 1
645
646             elif element["type"] == "plot":
647                 logging.info("  {:3d} Processing a plot ...".format(count))
648
649                 # Add layout to the plots:
650                 layout = element["layout"].get("layout", None)
651                 if layout is not None:
652                     element["layout"].pop("layout")
653                     try:
654                         for key, val in (self.configuration["plot-layouts"]
655                                          [layout].items()):
656                             element["layout"][key] = val
657                     except KeyError:
658                         raise PresentationError("Layout {0} is not defined in "
659                                                 "the configuration section.".
660                                                 format(layout))
661                 self._specification["plots"].append(element)
662                 count += 1
663
664             elif element["type"] == "file":
665                 logging.info("  {:3d} Processing a file ...".format(count))
666                 try:
667                     element["dir-tables"] = self._replace_tags(
668                         element["dir-tables"],
669                         self._specification["environment"]["paths"])
670                 except KeyError:
671                     pass
672                 self._specification["files"].append(element)
673                 count += 1
674
675             elif element["type"] == "cpta":
676                 logging.info("  {:3d} Processing Continuous Performance "
677                              "Trending and Analysis ...".format(count))
678
679                 for plot in element["plots"]:
680                     # Add layout to the plots:
681                     layout = plot.get("layout", None)
682                     if layout is not None:
683                         try:
684                             plot["layout"] = \
685                                 self.configuration["plot-layouts"][layout]
686                         except KeyError:
687                             raise PresentationError(
688                                 "Layout {0} is not defined in the "
689                                 "configuration section.".format(layout))
690                     # Add data sets:
691                     if isinstance(plot.get("data", None), str):
692                         data_set = plot["data"]
693                         try:
694                             plot["data"] = \
695                                 self.configuration["data-sets"][data_set]
696                         except KeyError:
697                             raise PresentationError(
698                                 "Data set {0} is not defined in "
699                                 "the configuration section.".
700                                 format(data_set))
701                 self._specification["cpta"] = element
702                 count += 1
703
704         logging.info("Done.")
705
706     def read_specification(self):
707         """Parse specification in the specification YAML file.
708
709         :raises: PresentationError if an error occurred while parsing the
710         specification file.
711         """
712         try:
713             self._cfg_yaml = load(self._cfg_file)
714         except YAMLError as err:
715             raise PresentationError(msg="An error occurred while parsing the "
716                                         "specification file.",
717                                     details=str(err))
718
719         self._parse_env()
720         self._parse_configuration()
721         self._parse_input()
722         self._parse_output()
723         self._parse_static()
724         self._parse_elements()
725
726         logging.debug("Specification: \n{}".
727                       format(pformat(self._specification)))