]> git.ipfire.org Git - nitsi.git/blame - test.py
Move the serial_connection class into a seperated file
[nitsi.git] / test.py
CommitLineData
5e7f6db7
JS
1#!/usr/bin/python3
2
5e7f6db7
JS
3
4import libvirt
5
6import xml.etree.ElementTree as ET
7
5e7f6db7
JS
8import os
9
10import configparser
11
14cd493f
JS
12from disk import disk
13
5e7f6db7
JS
14class log():
15 def __init__(self, log_level):
16 self.log_level = log_level
17
18 def debug(self, string):
19 if self.log_level >= 4:
20 print("DEBUG: {}".format(string))
21
22 def error(self, string):
23 print("ERROR: {}".format(string))
24
25class libvirt_con():
26 def __init__(self, uri):
27 self.log = log(4)
28 self.uri = uri
29 self.connection = None
30
31 def get_domain_from_name(self, name):
32 dom = self.con.lookupByName(name)
33
34 if dom == None:
35 raise BaseException
36 return dom
37
38 @property
39 def con(self):
40 if self.connection == None:
41 try:
42 self.connection = libvirt.open(self.uri)
43 except BaseException as error:
44 self.log.error("Could not connect to: {}".format(self.uri))
45
46 self.log.debug("Connected to: {}".format(self.uri))
47 return self.connection
48
49 return self.connection
50
51
632339b0 52class machine():
9bd4af37 53 def __init__(self, vm_xml_file, snapshot_xml_file, image, root_uid, username, password):
5e7f6db7
JS
54 self.log = log(4)
55 self.con = libvirt_con("qemu:///system")
56 try:
57 with open(vm_xml_file) as fobj:
58 self.vm_xml = fobj.read()
59 except FileNotFoundError as error:
60 self.log.error("No such file: {}".format(vm_xml_file))
61
62 try:
63 with open(snapshot_xml_file) as fobj:
64 self.snapshot_xml = fobj.read()
65 except FileNotFoundError as error:
66 self.log.error("No such file: {}".format(snapshot_xml_file))
67
68 self.image = image
69
70 if not os.path.isfile(self.image):
dd542251 71 self.log.error("No such file: {}".format(self.image))
5e7f6db7
JS
72
73 self.root_uid = root_uid
14cd493f 74 self.disk = disk(image)
5e7f6db7 75
9bd4af37
JS
76 self.username = username
77 self.password = password
78
5e7f6db7
JS
79 def define(self):
80 self.dom = self.con.con.defineXML(self.vm_xml)
81 if self.dom == None:
82 self.log.error("Could not define VM")
83 raise BaseException
84
85 def start(self):
86 if self.dom.create() < 0:
87 self.log.error("Could not start VM")
88 raise BaseException
89
90 def shutdown(self):
91 if self.is_running():
92 if self.dom.shutdown() < 0:
93 self.log.error("Could not shutdown VM")
94 raise BaseException
95 else:
96 self.log.error("Domain is not running")
97
98 def undefine(self):
99 self.dom.undefine()
100
101 def create_snapshot(self):
102
103 self.snapshot = self.dom.snapshotCreateXML(self.snapshot_xml)
104
105 if not self.snapshot:
106 self.log.error("Could not create snapshot")
107 raise BaseException
108
109 def revert_snapshot(self):
5e7f6db7 110 self.dom.revertToSnapshot(self.snapshot)
ec6e622d 111 self.snapshot.delete()
5e7f6db7
JS
112
113 def is_running(self):
114
115 state, reason = self.dom.state()
116
117 if state == libvirt.VIR_DOMAIN_RUNNING:
118 return True
119 else:
120 return False
121
122 def get_serial_device(self):
123
124 if not self.is_running():
125 raise BaseException
126
127 xml_root = ET.fromstring(self.dom.XMLDesc(0))
128
129 elem = xml_root.find("./devices/serial/source")
130 return elem.get("path")
131
132 def check_is_booted_up(self):
721af24c 133 serial_con = serial_connection(self.get_serial_device())
5e7f6db7
JS
134
135 serial_con.write("\n")
136 # This will block till the domain is booted up
137 serial_con.read(1)
138
139 #serial_con.close()
140
9bd4af37 141 def login(self):
5e7f6db7 142 try:
721af24c 143 self.serial_con = serial_connection(self.get_serial_device(), username=self.username)
9bd4af37 144 self.serial_con.login(self.password)
5e7f6db7
JS
145 except BaseException as e:
146 self.log.error("Could not connect to the domain via serial console")
147
148 def cmd(self, cmd):
149 return self.serial_con.command(cmd)
150
14cd493f
JS
151 def copy_in(self, fr, to):
152 try:
153 self.disk.mount(self.root_uid, "/")
154 self.disk.copy_in(fr, to)
155 except BaseException as e:
156 self.log.error(e)
157 finally:
158 self.disk.umount("/")
159 self.disk.close()
5e7f6db7 160
0486853d 161
5e7f6db7 162
5e7f6db7
JS
163# A class which define and undefine a virtual network based on an xml file
164class network():
48d5fb0a 165 def __init__(self, network_xml_file):
5e7f6db7 166 self.log = log(4)
48d5fb0a
JS
167 self.con = libvirt_con("qemu:///system")
168 try:
169 with open(network_xml_file) as fobj:
170 self.network_xml = fobj.read()
171 except FileNotFoundError as error:
172 self.log.error("No such file: {}".format(vm_xml_file))
173
174 def define(self):
175 self.network = self.con.con.networkDefineXML(self.network_xml)
176
177 if network == None:
178 self.log.error("Failed to define virtual network")
179
180 def start(self):
181 self.network.create()
182
183 def undefine(self):
184 self.network.destroy()
185
186
5e7f6db7 187
41ab240e
JS
188class RecipeExeption(Exception):
189 pass
190
191
192
5e7f6db7
JS
193# Should read the test, check if the syntax are valid
194# and return tuples with the ( host, command ) structure
195class recipe():
4505a887 196 def __init__(self, path, circle=[]):
5e7f6db7
JS
197 self.log = log(4)
198 self.recipe_file = path
4505a887
JS
199 self.path = os.path.dirname(self.recipe_file)
200 self.log.debug("Path of recipe is: {}".format(self.recipe_file))
41ab240e 201 self._recipe = None
2f35f899
JS
202 self._machines = None
203
204 self.in_recursion = True
205 if len(circle) == 0:
206 self.in_recursion = False
207
4505a887
JS
208 self.circle = circle
209 self.log.debug(circle)
210 self.log.debug(self.circle)
41ab240e 211
5e7f6db7
JS
212 if not os.path.isfile(self.recipe_file):
213 self.log.error("No such file: {}".format(self.recipe_file))
214
215 try:
216 with open(self.recipe_file) as fobj:
217 self.raw_recipe = fobj.readlines()
218 except FileNotFoundError as error:
219 self.log.error("No such file: {}".format(vm_xml_file))
220
41ab240e
JS
221 @property
222 def recipe(self):
223 if not self._recipe:
224 self.parse()
225
226 return self._recipe
227
2f35f899
JS
228 @property
229 def machines(self):
230 if not self._machines:
231 self._machines = []
232 for line in self._recipe:
233 if line[0] != "all" and line[0] not in self._machines:
234 self._machines.append(line[0])
235
236 return self._machines
237
41ab240e
JS
238 def parse(self):
239 self._recipe = []
240 i = 1
5e7f6db7 241 for line in self.raw_recipe:
41ab240e
JS
242 raw_line = line.split(":")
243 if len(raw_line) < 2:
244 self.log.error("Error parsing the recipe in line {}".format(i))
245 raise RecipeExeption
07bf49a3 246 cmd = raw_line[1].strip()
41ab240e
JS
247 raw_line = raw_line[0].strip().split(" ")
248 if len(raw_line) == 0:
249 self.log.error("Failed to parse the recipe in line {}".format(i))
250 raise RecipeExeption
07bf49a3
JS
251
252 if raw_line[0].strip() == "":
41ab240e
JS
253 self.log.error("Failed to parse the recipe in line {}".format(i))
254 raise RecipeExeption
07bf49a3
JS
255
256 machine = raw_line[0].strip()
257
258 if len(raw_line) == 2:
259 extra = raw_line[1].strip()
260 else:
261 extra = ""
262
263 # We could get a machine here or a include statement
264 if machine == "include":
265 path = cmd.strip()
266 path = os.path.normpath(self.path + "/" + path)
267 path = path + "/recipe"
268 if path in self.circle:
269 self.log.error("Detect import loop!")
270 raise RecipeExeption
271 self.circle.append(path)
272 recipe_to_include = recipe(path, circle=self.circle)
273
274 if machine == "include":
4505a887
JS
275 self._recipe.extend(recipe_to_include.recipe)
276 else:
67c8eded
JS
277 # Support also something like 'alice,bob: echo'
278 machines = machine.split(",")
279 for machine in machines:
280 self._recipe.append((machine.strip(), extra.strip(), cmd.strip()))
41ab240e 281 i = i + 1
5e7f6db7 282
2f35f899
JS
283 if not self.in_recursion:
284 tmp_recipe = []
285 for line in self._recipe:
286 if line[0] != "all":
287 tmp_recipe.append(line)
288 else:
289 for machine in self.machines:
290 tmp_recipe.append((machine.strip(), line[1], line[2]))
291
292 self._recipe = tmp_recipe
293
294
5e7f6db7 295
5e7f6db7
JS
296class test():
297 def __init__(self, path):
298 self.log = log(4)
299 try:
300 self.path = os.path.abspath(path)
301 except BaseException as e:
302 self.log.error("Could not get absolute path")
303
304 self.log.debug(self.path)
305
306 self.settings_file = "{}/settings".format(self.path)
307 if not os.path.isfile(self.settings_file):
308 self.log.error("No such file: {}".format(self.settings_file))
309
310 self.recipe_file = "{}/recipe".format(self.path)
311 if not os.path.isfile(self.recipe_file):
312 self.log.error("No such file: {}".format(self.recipe_file))
313
314 def read_settings(self):
315 self.config = configparser.ConfigParser()
316 self.config.read(self.settings_file)
317 self.name = self.config["DEFAULT"]["Name"]
318 self.description = self.config["DEFAULT"]["Description"]
14cd493f
JS
319 self.copy_to = self.config["DEFAULT"]["Copy_to"]
320 self.copy_from = self.config["DEFAULT"]["Copy_from"]
321 self.copy_from = self.copy_from.split(",")
322
323 tmp = []
324 for file in self.copy_from:
325 file = file.strip()
326 file = os.path.normpath(self.path + "/" + file)
327 tmp.append(file)
328
329 self.copy_from = tmp
5e7f6db7
JS
330
331 self.virtual_environ_name = self.config["VIRTUAL_ENVIRONMENT"]["Name"]
332 self.virtual_environ_path = self.config["VIRTUAL_ENVIRONMENT"]["Path"]
333 self.virtual_environ_path = os.path.normpath(self.path + "/" + self.virtual_environ_path)
334
335 def virtual_environ_setup(self):
336 self.virtual_environ = virtual_environ(self.virtual_environ_path)
337
338 self.virtual_networks = self.virtual_environ.get_networks()
339
340 self.virtual_machines = self.virtual_environ.get_machines()
341
342 def virtual_environ_start(self):
3fa89b7c
JS
343 for name in self.virtual_environ.network_names:
344 self.virtual_networks[name].define()
345 self.virtual_networks[name].start()
5e7f6db7 346
3fa89b7c
JS
347 for name in self.virtual_environ.machine_names:
348 self.virtual_machines[name].define()
349 self.virtual_machines[name].create_snapshot()
14cd493f 350 self.virtual_machines[name].copy_in(self.copy_from, self.copy_to)
3fa89b7c 351 self.virtual_machines[name].start()
5e7f6db7 352
3fa89b7c
JS
353 self.log.debug("Try to login on all machines")
354 for name in self.virtual_environ.machine_names:
355 self.virtual_machines[name].login()
5e7f6db7 356
3fa89b7c
JS
357 def load_recipe(self):
358 try:
359 self.recipe = recipe(self.recipe_file)
360 except BaseException:
361 self.log.error("Failed to load recipe")
362 exit(1)
363
364 def run_recipe(self):
365 for line in self.recipe.recipe:
366 return_value = self.virtual_machines[line[0]].cmd(line[2])
bce7d520
JS
367 self.log.debug("Return value is: {}".format(return_value))
368 if return_value != "0" and line[1] == "":
369 self.log.error("Failed to execute command '{}' on {}, return code: {}".format(line[2],line[0], return_value))
3fa89b7c 370 return False
bce7d520
JS
371 elif return_value == "0" and line[1] == "!":
372 self.log.error("Succeded to execute command '{}' on {}, return code: {}".format(line[2],line[0],return_value))
3fa89b7c 373 return False
bce7d520
JS
374 else:
375 self.log.debug("Command '{}' on {} returned with: {}".format(line[2],line[0],return_value))
3fa89b7c
JS
376
377 def virtual_environ_stop(self):
378 for name in self.virtual_environ.machine_names:
379 self.virtual_machines[name].shutdown()
380 self.virtual_machines[name].revert_snapshot()
381 self.virtual_machines[name].undefine()
382
383 for name in self.virtual_environ.network_names:
384 self.virtual_networks[name].undefine()
5e7f6db7
JS
385
386
5e7f6db7
JS
387# Should return all vms and networks in a list
388# and should provide the path to the necessary xml files
389class virtual_environ():
390 def __init__(self, path):
391 self.log = log(4)
392 try:
393 self.path = os.path.abspath(path)
394 except BaseException as e:
395 self.log.error("Could not get absolute path")
396
397 self.log.debug(self.path)
398
399 self.settings_file = "{}/settings".format(self.path)
400 if not os.path.isfile(self.settings_file):
401 self.log.error("No such file: {}".format(self.settings_file))
402
403 self.log.debug(self.settings_file)
404 self.config = configparser.ConfigParser()
405 self.config.read(self.settings_file)
406 self.name = self.config["DEFAULT"]["name"]
407 self.machines_string = self.config["DEFAULT"]["machines"]
408 self.networks_string = self.config["DEFAULT"]["networks"]
409
410 self.machines = []
411 for machine in self.machines_string.split(","):
412 self.machines.append(machine.strip())
413
414 self.networks = []
415 for network in self.networks_string.split(","):
416 self.networks.append(network.strip())
417
418 self.log.debug(self.machines)
419 self.log.debug(self.networks)
420
421 def get_networks(self):
422 networks = {}
423 for _network in self.networks:
424 self.log.debug(_network)
425 networks.setdefault(_network, network(os.path.normpath(self.path + "/" + self.config[_network]["xml_file"])))
426 return networks
427
428 def get_machines(self):
429 machines = {}
430 for _machine in self.machines:
431 self.log.debug(_machine)
632339b0 432 machines.setdefault(_machine, machine(
5e7f6db7 433 os.path.normpath(self.path + "/" + self.config[_machine]["xml_file"]),
2d53bc4f
JS
434 os.path.normpath(self.path + "/" + self.config[_machine]["snapshot_xml_file"]),
435 self.config[_machine]["image"],
436 self.config[_machine]["root_uid"],
437 self.config[_machine]["username"],
438 self.config[_machine]["password"]))
5e7f6db7
JS
439
440 return machines
441
2d53bc4f
JS
442 @property
443 def machine_names(self):
444 return self.machines
445
446 @property
447 def network_names(self):
448 return self.networks
449
5e7f6db7 450
5e7f6db7
JS
451if __name__ == "__main__":
452 import argparse
453
454 parser = argparse.ArgumentParser()
455
456 parser.add_argument("-d", "--directory", dest="dir")
457
458 args = parser.parse_args()
459
5e7f6db7
JS
460 currenttest = test(args.dir)
461 currenttest.read_settings()
10f65154
JS
462 currenttest.virtual_environ_setup()
463 currenttest.load_recipe()
0a4b6cfb
JS
464 try:
465 currenttest.virtual_environ_start()
466 currenttest.run_recipe()
467 except BaseException as e:
468 print(e)
469 finally:
470 currenttest.virtual_environ_stop()
471