Allow including of recipe files
[nitsi.git] / src / nitsi / recipe.py
1 #!/usr/bin/python3
2
3 import logging
4 import os
5
6 logger = logging.getLogger("nitsi.recipe")
7
8
9
10 class RecipeExeption(Exception):
11     def __init__(self, message):
12         self.message = message
13
14
15
16 # Should read the test, check if the syntax are valid
17 # and return tuples with the ( host, command ) structure
18 class Recipe():
19     def __init__(self, path, circle=[], machines=[]):
20         self.recipe_file = path
21         try:
22             self.path = os.path.dirname(self.recipe_file)
23             self.path = os.path.abspath(self.path)
24             self.name = os.path.basename(self.path)
25         except BaseException as e:
26             logger.error("Failed to get the path to this recipe")
27             raise e
28
29         self.log = logger.getChild(self.name)
30         self.log.debug("Path of recipe is: {}".format(self.recipe_file))
31         self._recipe = None
32         self._machines = machines
33
34         self.log.debug("Machine names we use when we substitute the all statement: {}".format(self._machines))
35
36         self.in_recursion = True
37         if len(circle) == 0:
38             self.in_recursion = False
39
40         self.circle = circle
41         self.log.debug("Recipes we have already included: {}".format(self.circle))
42
43         if not os.path.isfile(self.recipe_file):
44             self.log.error("{} is not a file".format(self.recipe_file))
45             raise RecipeExeption("{} is not a file".format(self.recipe_file))
46
47         try:
48             with open(self.recipe_file) as fobj:
49                 self.raw_recipe = fobj.readlines()
50         except FileNotFoundError as error:
51             self.log.error("No such file: {}".format(self.recipe_file))
52             raise error
53
54     @property
55     def recipe(self):
56         if not self._recipe:
57             self.parse()
58
59         return self._recipe
60
61     @property
62     def machines(self):
63         return self._machines
64
65     def parse(self):
66         self._recipe = []
67         i = 1
68         for line in self.raw_recipe:
69             # Check if the line is empty
70             if line.strip() == "":
71                 self.log.debug("Skipping empty line {}".format(i))
72                 i = i + 1
73                 continue
74
75             # Check if the line is a comment
76             if line.strip().startswith("#"):
77                 self.log.debug("Skipping comment in line {}".format(i))
78                 i = i + 1
79                 continue
80
81             raw_line = line.split(":", 1)
82             if len(raw_line) < 2:
83                 self.log.error("Error parsing the recipe in line {}".format(i))
84                 raise RecipeExeption("Error parsing the recipe in line {}".format(i))
85             cmd = raw_line[1].strip()
86             raw_line = raw_line[0].strip().split(" ")
87             if len(raw_line) == 0:
88                 self.log.error("Failed to parse the recipe in line {}".format(i))
89                 raise RecipeExeption("Failed to parse the recipe in line {}".format(i))
90
91             if raw_line[0].strip() == "":
92                     self.log.error("Failed to parse the recipe in line {}".format(i))
93                     raise RecipeExeption("Failed to parse the recipe in line {}".format(i))
94
95             machine = raw_line[0].strip()
96
97             if len(raw_line) == 2:
98                 extra = raw_line[1].strip()
99             else:
100                 extra = ""
101
102             # We could get a machine here or a include statement
103             if machine == "include":
104                 path = cmd.strip()
105                 path = os.path.normpath(self.path + "/" + path)
106
107                 # If we did not get a valid file we asume that we get a valid path to a test.
108                 if os.path.isdir(path):
109                     path = path + "/recipe"
110
111                 if path in self.circle:
112                     self.log.error("Detect import loop!")
113                     raise RecipeExeption("Detect import loop!")
114                 self.circle.append(path)
115                 recipe_to_include = Recipe(path, circle=self.circle)
116
117             if machine == "include":
118                 self._recipe.extend(recipe_to_include.recipe)
119             else:
120                 # Support also something like 'alice,bob: echo'
121                 machines = machine.split(",")
122                 for machine in machines:
123                     self._recipe.append((machine.strip(), extra.strip(), cmd.strip()))
124             i = i + 1
125
126             if not self.in_recursion:
127                 tmp_recipe = []
128                 for line in self._recipe:
129                     if line[0] != "all":
130                         tmp_recipe.append(line)
131                     else:
132                         for machine in self.machines:
133                             tmp_recipe.append((machine.strip(), line[1], line[2]))
134
135                 self._recipe = tmp_recipe