Move machine class into an own file
[nitsi.git] / test.py
1 #!/usr/bin/python3
2
3
4 import libvirt
5
6 import os
7
8 import configparser
9
10 from disk import disk
11
12 class log():
13     def __init__(self, log_level):
14         self.log_level = log_level
15
16     def debug(self, string):
17         if self.log_level >= 4:
18             print("DEBUG: {}".format(string))
19
20     def error(self, string):
21         print("ERROR: {}".format(string))
22
23 class libvirt_con():
24     def __init__(self, uri):
25         self.log = log(4)
26         self.uri = uri
27         self.connection = None
28
29     def get_domain_from_name(self, name):
30         dom = self.con.lookupByName(name)
31
32         if dom == None:
33             raise BaseException
34         return dom
35
36     @property
37     def con(self):
38         if self.connection == None:
39             try:
40                 self.connection = libvirt.open(self.uri)
41             except BaseException as error:
42                 self.log.error("Could not connect to: {}".format(self.uri))
43
44             self.log.debug("Connected to: {}".format(self.uri))
45             return self.connection
46
47         return self.connection
48
49
50
51 # A class which define and undefine a virtual network based on an xml file
52 class network():
53     def __init__(self, network_xml_file):
54         self.log = log(4)
55         self.con = libvirt_con("qemu:///system")
56         try:
57             with open(network_xml_file) as fobj:
58                 self.network_xml = fobj.read()
59         except FileNotFoundError as error:
60             self.log.error("No such file: {}".format(vm_xml_file))
61
62     def define(self):
63         self.network = self.con.con.networkDefineXML(self.network_xml)
64
65         if network == None:
66             self.log.error("Failed to define virtual network")
67
68     def start(self):
69         self.network.create()
70
71     def undefine(self):
72         self.network.destroy()
73
74
75
76 class RecipeExeption(Exception):
77     pass
78
79
80
81 # Should read the test, check if the syntax are valid
82 # and return tuples with the ( host, command ) structure
83 class recipe():
84     def __init__(self, path, circle=[]):
85         self.log = log(4)
86         self.recipe_file = path
87         self.path = os.path.dirname(self.recipe_file)
88         self.log.debug("Path of recipe is: {}".format(self.recipe_file))
89         self._recipe = None
90         self._machines = None
91
92         self.in_recursion = True
93         if len(circle) == 0:
94             self.in_recursion = False
95
96         self.circle = circle
97         self.log.debug(circle)
98         self.log.debug(self.circle)
99
100         if not os.path.isfile(self.recipe_file):
101             self.log.error("No such file: {}".format(self.recipe_file))
102
103         try:
104             with open(self.recipe_file) as fobj:
105                 self.raw_recipe = fobj.readlines()
106         except FileNotFoundError as error:
107             self.log.error("No such file: {}".format(vm_xml_file))
108
109     @property
110     def recipe(self):
111         if not self._recipe:
112             self.parse()
113
114         return self._recipe
115
116     @property
117     def machines(self):
118         if not self._machines:
119             self._machines = []
120             for line in self._recipe:
121                 if line[0] != "all" and line[0] not in self._machines:
122                     self._machines.append(line[0])
123
124         return self._machines
125
126     def parse(self):
127         self._recipe = []
128         i = 1
129         for line in self.raw_recipe:
130             raw_line = line.split(":")
131             if len(raw_line) < 2:
132                 self.log.error("Error parsing the recipe in line {}".format(i))
133                 raise RecipeExeption
134             cmd = raw_line[1].strip()
135             raw_line = raw_line[0].strip().split(" ")
136             if len(raw_line) == 0:
137                 self.log.error("Failed to parse the recipe in line {}".format(i))
138                 raise RecipeExeption
139
140             if raw_line[0].strip() == "":
141                     self.log.error("Failed to parse the recipe in line {}".format(i))
142                     raise RecipeExeption
143
144             machine = raw_line[0].strip()
145
146             if len(raw_line) == 2:
147                 extra = raw_line[1].strip()
148             else:
149                 extra = ""
150
151             # We could get a machine here or a include statement
152             if machine == "include":
153                 path = cmd.strip()
154                 path = os.path.normpath(self.path + "/" + path)
155                 path = path + "/recipe"
156                 if path in self.circle:
157                     self.log.error("Detect import loop!")
158                     raise RecipeExeption
159                 self.circle.append(path)
160                 recipe_to_include = recipe(path, circle=self.circle)
161
162             if machine == "include":
163                 self._recipe.extend(recipe_to_include.recipe)
164             else:
165                 # Support also something like 'alice,bob: echo'
166                 machines = machine.split(",")
167                 for machine in machines:
168                     self._recipe.append((machine.strip(), extra.strip(), cmd.strip()))
169             i = i + 1
170
171             if not self.in_recursion:
172                 tmp_recipe = []
173                 for line in self._recipe:
174                     if line[0] != "all":
175                         tmp_recipe.append(line)
176                     else:
177                         for machine in self.machines:
178                             tmp_recipe.append((machine.strip(), line[1], line[2]))
179
180                 self._recipe = tmp_recipe
181
182
183
184 class test():
185     def __init__(self, path):
186         self.log = log(4)
187         try:
188             self.path = os.path.abspath(path)
189         except BaseException as e:
190             self.log.error("Could not get absolute path")
191
192         self.log.debug(self.path)
193
194         self.settings_file = "{}/settings".format(self.path)
195         if not os.path.isfile(self.settings_file):
196             self.log.error("No such file: {}".format(self.settings_file))
197
198         self.recipe_file = "{}/recipe".format(self.path)
199         if not os.path.isfile(self.recipe_file):
200             self.log.error("No such file: {}".format(self.recipe_file))
201
202     def read_settings(self):
203         self.config = configparser.ConfigParser()
204         self.config.read(self.settings_file)
205         self.name = self.config["DEFAULT"]["Name"]
206         self.description = self.config["DEFAULT"]["Description"]
207         self.copy_to = self.config["DEFAULT"]["Copy_to"]
208         self.copy_from = self.config["DEFAULT"]["Copy_from"]
209         self.copy_from = self.copy_from.split(",")
210
211         tmp = []
212         for file in self.copy_from:
213             file = file.strip()
214             file = os.path.normpath(self.path + "/" + file)
215             tmp.append(file)
216
217         self.copy_from = tmp
218
219         self.virtual_environ_name = self.config["VIRTUAL_ENVIRONMENT"]["Name"]
220         self.virtual_environ_path = self.config["VIRTUAL_ENVIRONMENT"]["Path"]
221         self.virtual_environ_path = os.path.normpath(self.path + "/" + self.virtual_environ_path)
222
223     def virtual_environ_setup(self):
224         self.virtual_environ = virtual_environ(self.virtual_environ_path)
225
226         self.virtual_networks = self.virtual_environ.get_networks()
227
228         self.virtual_machines = self.virtual_environ.get_machines()
229
230     def virtual_environ_start(self):
231         for name in self.virtual_environ.network_names:
232             self.virtual_networks[name].define()
233             self.virtual_networks[name].start()
234
235         for name in self.virtual_environ.machine_names:
236             self.virtual_machines[name].define()
237             self.virtual_machines[name].create_snapshot()
238             self.virtual_machines[name].copy_in(self.copy_from, self.copy_to)
239             self.virtual_machines[name].start()
240
241         self.log.debug("Try to login on all machines")
242         for name in self.virtual_environ.machine_names:
243             self.virtual_machines[name].login()
244
245     def load_recipe(self):
246         try:
247             self.recipe = recipe(self.recipe_file)
248         except BaseException:
249             self.log.error("Failed to load recipe")
250             exit(1)
251
252     def run_recipe(self):
253         for line in self.recipe.recipe:
254             return_value = self.virtual_machines[line[0]].cmd(line[2])
255             self.log.debug("Return value is: {}".format(return_value))
256             if return_value != "0" and line[1] == "":
257                 self.log.error("Failed to execute command '{}' on {}, return code: {}".format(line[2],line[0], return_value))
258                 return False
259             elif return_value == "0" and line[1] == "!":
260                 self.log.error("Succeded to execute command '{}' on {}, return code: {}".format(line[2],line[0],return_value))
261                 return False
262             else:
263                 self.log.debug("Command '{}' on {} returned with: {}".format(line[2],line[0],return_value))
264
265     def virtual_environ_stop(self):
266         for name in self.virtual_environ.machine_names:
267             self.virtual_machines[name].shutdown()
268             self.virtual_machines[name].revert_snapshot()
269             self.virtual_machines[name].undefine()
270
271         for name in self.virtual_environ.network_names:
272             self.virtual_networks[name].undefine()
273
274
275 # Should return all vms and networks in a list
276 # and should provide the path to the necessary xml files
277 class virtual_environ():
278     def __init__(self, path):
279         self.log = log(4)
280         try:
281             self.path = os.path.abspath(path)
282         except BaseException as e:
283             self.log.error("Could not get absolute path")
284
285         self.log.debug(self.path)
286
287         self.settings_file = "{}/settings".format(self.path)
288         if not os.path.isfile(self.settings_file):
289             self.log.error("No such file: {}".format(self.settings_file))
290
291         self.log.debug(self.settings_file)
292         self.config = configparser.ConfigParser()
293         self.config.read(self.settings_file)
294         self.name = self.config["DEFAULT"]["name"]
295         self.machines_string = self.config["DEFAULT"]["machines"]
296         self.networks_string = self.config["DEFAULT"]["networks"]
297
298         self.machines = []
299         for machine in self.machines_string.split(","):
300             self.machines.append(machine.strip())
301
302         self.networks = []
303         for network in self.networks_string.split(","):
304             self.networks.append(network.strip())
305
306         self.log.debug(self.machines)
307         self.log.debug(self.networks)
308
309     def get_networks(self):
310         networks = {}
311         for _network in self.networks:
312             self.log.debug(_network)
313             networks.setdefault(_network, network(os.path.normpath(self.path + "/" + self.config[_network]["xml_file"])))
314         return networks
315
316     def get_machines(self):
317         machines = {}
318         for _machine in self.machines:
319             self.log.debug(_machine)
320             machines.setdefault(_machine, machine(
321                 os.path.normpath(self.path + "/" + self.config[_machine]["xml_file"]),
322                 os.path.normpath(self.path + "/" + self.config[_machine]["snapshot_xml_file"]),
323                 self.config[_machine]["image"],
324                 self.config[_machine]["root_uid"],
325                 self.config[_machine]["username"],
326                 self.config[_machine]["password"]))
327
328         return machines
329
330     @property
331     def machine_names(self):
332         return self.machines
333
334     @property
335     def network_names(self):
336         return self.networks
337
338
339 if __name__ == "__main__":
340     import argparse
341
342     parser = argparse.ArgumentParser()
343
344     parser.add_argument("-d", "--directory", dest="dir")
345
346     args = parser.parse_args()
347
348     currenttest = test(args.dir)
349     currenttest.read_settings()
350     currenttest.virtual_environ_setup()
351     currenttest.load_recipe()
352     try:
353         currenttest.virtual_environ_start()
354         currenttest.run_recipe()
355     except BaseException as e:
356         print(e)
357     finally:
358         currenttest.virtual_environ_stop()
359