Return exit code of executed commands
[nitsi.git] / test.py
CommitLineData
5e7f6db7
JS
1#!/usr/bin/python3
2
3import serial
4
5import re
6from time import sleep
7import sys
8
9import libvirt
10
11import xml.etree.ElementTree as ET
12
5e7f6db7
JS
13import os
14
15import configparser
16
17class log():
18 def __init__(self, log_level):
19 self.log_level = log_level
20
21 def debug(self, string):
22 if self.log_level >= 4:
23 print("DEBUG: {}".format(string))
24
25 def error(self, string):
26 print("ERROR: {}".format(string))
27
28class libvirt_con():
29 def __init__(self, uri):
30 self.log = log(4)
31 self.uri = uri
32 self.connection = None
33
34 def get_domain_from_name(self, name):
35 dom = self.con.lookupByName(name)
36
37 if dom == None:
38 raise BaseException
39 return dom
40
41 @property
42 def con(self):
43 if self.connection == None:
44 try:
45 self.connection = libvirt.open(self.uri)
46 except BaseException as error:
47 self.log.error("Could not connect to: {}".format(self.uri))
48
49 self.log.debug("Connected to: {}".format(self.uri))
50 return self.connection
51
52 return self.connection
53
54
55class vm():
9bd4af37 56 def __init__(self, vm_xml_file, snapshot_xml_file, image, root_uid, username, password):
5e7f6db7
JS
57 self.log = log(4)
58 self.con = libvirt_con("qemu:///system")
59 try:
60 with open(vm_xml_file) as fobj:
61 self.vm_xml = fobj.read()
62 except FileNotFoundError as error:
63 self.log.error("No such file: {}".format(vm_xml_file))
64
65 try:
66 with open(snapshot_xml_file) as fobj:
67 self.snapshot_xml = fobj.read()
68 except FileNotFoundError as error:
69 self.log.error("No such file: {}".format(snapshot_xml_file))
70
71 self.image = image
72
73 if not os.path.isfile(self.image):
dd542251 74 self.log.error("No such file: {}".format(self.image))
5e7f6db7
JS
75
76 self.root_uid = root_uid
77
9bd4af37
JS
78 self.username = username
79 self.password = password
80
5e7f6db7
JS
81 def define(self):
82 self.dom = self.con.con.defineXML(self.vm_xml)
83 if self.dom == None:
84 self.log.error("Could not define VM")
85 raise BaseException
86
87 def start(self):
88 if self.dom.create() < 0:
89 self.log.error("Could not start VM")
90 raise BaseException
91
92 def shutdown(self):
93 if self.is_running():
94 if self.dom.shutdown() < 0:
95 self.log.error("Could not shutdown VM")
96 raise BaseException
97 else:
98 self.log.error("Domain is not running")
99
100 def undefine(self):
101 self.dom.undefine()
102
103 def create_snapshot(self):
104
105 self.snapshot = self.dom.snapshotCreateXML(self.snapshot_xml)
106
107 if not self.snapshot:
108 self.log.error("Could not create snapshot")
109 raise BaseException
110
111 def revert_snapshot(self):
5e7f6db7 112 self.dom.revertToSnapshot(self.snapshot)
ec6e622d 113 self.snapshot.delete()
5e7f6db7
JS
114
115 def is_running(self):
116
117 state, reason = self.dom.state()
118
119 if state == libvirt.VIR_DOMAIN_RUNNING:
120 return True
121 else:
122 return False
123
124 def get_serial_device(self):
125
126 if not self.is_running():
127 raise BaseException
128
129 xml_root = ET.fromstring(self.dom.XMLDesc(0))
130
131 elem = xml_root.find("./devices/serial/source")
132 return elem.get("path")
133
134 def check_is_booted_up(self):
135 serial_con = connection(self.get_serial_device())
136
137 serial_con.write("\n")
138 # This will block till the domain is booted up
139 serial_con.read(1)
140
141 #serial_con.close()
142
9bd4af37 143 def login(self):
5e7f6db7 144 try:
9bd4af37
JS
145 self.serial_con = connection(self.get_serial_device(), username=self.username)
146 self.serial_con.login(self.password)
5e7f6db7
JS
147 except BaseException as e:
148 self.log.error("Could not connect to the domain via serial console")
149
150 def cmd(self, cmd):
151 return self.serial_con.command(cmd)
152
153
5e7f6db7
JS
154class connection():
155 def __init__(self, device, username=None):
156 self.buffer = b""
157 self.back_at_prompt_pattern = None
158 self.username = username
159 self.log = log(1)
160 self.con = serial.Serial(device)
5e7f6db7
JS
161
162 def read(self, size=1):
163 if len(self.buffer) >= size:
164 # throw away first size bytes in buffer
165 data = self.buffer[:size]
166 # Set the buffer to the non used bytes
167 self.buffer = self.buffer[size:]
168 return data
169 else:
170 data = self.buffer
171 # Set the size to the value we have to read now
172 size = size - len(self.buffer)
173 # Set the buffer empty
174 self.buffer = b""
175 return data + self.con.read(size)
176
177 def peek(self, size=1):
178 if len(self.buffer) <= size:
179 self.buffer += self.con.read(size=size - len(self.buffer))
180
181 return self.buffer[:size]
182
183 def readline(self):
184 self.log.debug(self.buffer)
185 self.buffer = self.buffer + self.con.read(self.con.in_waiting)
186 if b"\n" in self.buffer:
187 size = self.buffer.index(b"\n") + 1
188 self.log.debug("We have a whole line in the buffer")
189 self.log.debug(self.buffer)
190 self.log.debug("We split at {}".format(size))
191 data = self.buffer[:size]
192 self.buffer = self.buffer[size:]
193 self.log.debug(data)
194 self.log.debug(self.buffer)
195 return data
196
197 data = self.buffer
198 self.buffer = b""
199 return data + self.con.readline()
200
201 def back_at_prompt(self):
202 data = self.peek()
203 if not data == b"[":
204 return False
205
206 # We need to use self.in_waiting because with self.con.in_waiting we get
207 # not the complete string
208 size = len(self.buffer) + self.in_waiting
209 data = self.peek(size)
210
211
212 if self.back_at_prompt_pattern == None:
213 #self.back_at_prompt_pattern = r"^\[{}@.+\]#".format(self.username)
214 self.back_at_prompt_pattern = re.compile(r"^\[{}@.+\]#".format(self.username), re.MULTILINE)
215
216 if self.back_at_prompt_pattern.search(data.decode()):
217 return True
218 else:
219 return False
220
221 def log_console_line(self, line):
222 self.log.debug("Get in function log_console_line()")
223 sys.stdout.write(line)
224
225 @property
226 def in_waiting(self):
227 in_waiting_before = 0
228 sleep(0.5)
229
230 while in_waiting_before != self.con.in_waiting:
231 in_waiting_before = self.con.in_waiting
232 sleep(0.5)
233
234 return self.con.in_waiting
235
e54f9c23
JS
236 def line_in_buffer(self):
237 if b"\n" in self.buffer:
238 return True
239
240 return False
5e7f6db7
JS
241
242 def readline2(self, pattern=None):
243 string = ""
244 string2 = b""
245 if pattern:
246 pattern = re.compile(pattern)
247
248 while 1:
249 char = self.con.read(1)
250 string = string + char.decode("utf-8")
251 string2 = string2 + char
252 #print(char)
253 print(char.decode("utf-8"), end="")
254
255 #print(string2)
256 if pattern and pattern.match(string):
257 #print("get here1")
258 #print(string2)
259 return {"string" : string, "return-code" : 1}
260
261 if char == b"\n":
262 #print(char)
263 #print(string2)
264 #print("get here2")
265 return {"return-code" : 0}
266
267 def check_logged_in(self, username):
268 pattern = "^\[" + username + "@.+\]#"
269 data = self.readline(pattern=pattern)
270 if data["return-code"] == 1:
271 print("We are logged in")
272 return True
273 else:
274 print("We are not logged in")
275 return False
276
736f73db
JS
277 def print_lines_in_buffer(self):
278 while True:
279 self.log.debug("Fill buffer ...")
280 self.peek(len(self.buffer) + self.in_waiting)
281 self.log.debug("Current buffer length: {}".format(len(self.buffer)))
282 if self.line_in_buffer() == True:
283 while self.line_in_buffer() == True:
284 data = self.readline()
285 self.log_console_line(data.decode())
286 else:
287 self.log.debug("We have printed all lines in the buffer")
288 break
289
5e7f6db7
JS
290 def login(self, password):
291 if self.username == None:
292 self.log.error("Username cannot be blank")
293 return False
294
736f73db
JS
295 self.print_lines_in_buffer()
296
5e7f6db7
JS
297 # Hit enter to see what we get
298 self.con.write(b'\n')
299 # We get two new lines \r\n ?
300 data = self.readline()
301 self.log_console_line(data.decode())
302
736f73db 303 self.print_lines_in_buffer()
5e7f6db7
JS
304
305 if self.back_at_prompt():
306 self.log.debug("We are already logged in.")
307 return True
308
309 # Read all line till we get login:
310 while 1:
5e7f6db7
JS
311 # We need to use self.in_waiting because with self.con.in_waiting we get
312 # not the complete string
313 size = len(self.buffer) + self.in_waiting
314 data = self.peek(size)
315
316 pattern = r"^.*login: "
317 pattern = re.compile(pattern)
318
319 if pattern.search(data.decode()):
320 break
321 else:
322 self.log.debug("The pattern does not match")
323 self.log_console_line(self.readline().decode())
324
325 # We can login
326 string = "{}\n".format(self.username)
327 self.con.write(string.encode())
328 self.con.flush()
329 # read the login out of the buffer
330 data = self.readline()
331 self.log.debug("This is the login:{}".format(data))
332 self.log_console_line(data.decode())
333
334 # We need to wait her till we get the full string "Password:"
335 #This is useless but self.in_waiting will wait the correct amount of time
336 size = self.in_waiting
337
338 string = "{}\n".format(password)
339 self.con.write(string.encode())
340 self.con.flush()
341 # Print the 'Password:' line
342 data = self.readline()
343 self.log_console_line(data.decode())
344
345 while not self.back_at_prompt():
346 # This will fail if the login failed so we need to look for the failed keyword
347 data = self.readline()
348 self.log_console_line(data.decode())
349
350 return True
351
352 def write(self, string):
353 self.log.debug(string)
354 self.con.write(string.encode())
355 self.con.flush()
356
357 def command(self, command):
0486853d 358 self.write("{}; echo \"END: $?\"\n".format(command))
5e7f6db7
JS
359
360 # We need to read out the prompt for this command first
361 # If we do not do this we will break the loop immediately
362 # because the prompt for this command is still in the buffer
363 data = self.readline()
364 self.log_console_line(data.decode())
365
366 while not self.back_at_prompt():
367 data = self.readline()
368 self.log_console_line(data.decode())
369
0486853d
JS
370 # We saved our exit code in data (the last line)
371 self.log.debug(data.decode())
372 data = data.decode().replace("END: ", "")
373 self.log.debug(data)
374 self.log.debug(data.strip())
375 return data.strip()
376
5e7f6db7 377
5e7f6db7
JS
378# A class which define and undefine a virtual network based on an xml file
379class network():
48d5fb0a 380 def __init__(self, network_xml_file):
5e7f6db7 381 self.log = log(4)
48d5fb0a
JS
382 self.con = libvirt_con("qemu:///system")
383 try:
384 with open(network_xml_file) as fobj:
385 self.network_xml = fobj.read()
386 except FileNotFoundError as error:
387 self.log.error("No such file: {}".format(vm_xml_file))
388
389 def define(self):
390 self.network = self.con.con.networkDefineXML(self.network_xml)
391
392 if network == None:
393 self.log.error("Failed to define virtual network")
394
395 def start(self):
396 self.network.create()
397
398 def undefine(self):
399 self.network.destroy()
400
401
5e7f6db7 402
41ab240e
JS
403class RecipeExeption(Exception):
404 pass
405
406
407
5e7f6db7
JS
408# Should read the test, check if the syntax are valid
409# and return tuples with the ( host, command ) structure
410class recipe():
411 def __init__(self, path):
412 self.log = log(4)
413 self.recipe_file = path
41ab240e
JS
414 self._recipe = None
415
5e7f6db7
JS
416 if not os.path.isfile(self.recipe_file):
417 self.log.error("No such file: {}".format(self.recipe_file))
418
419 try:
420 with open(self.recipe_file) as fobj:
421 self.raw_recipe = fobj.readlines()
422 except FileNotFoundError as error:
423 self.log.error("No such file: {}".format(vm_xml_file))
424
41ab240e
JS
425 @property
426 def recipe(self):
427 if not self._recipe:
428 self.parse()
429
430 return self._recipe
431
432 def parse(self):
433 self._recipe = []
434 i = 1
5e7f6db7 435 for line in self.raw_recipe:
41ab240e
JS
436 raw_line = line.split(":")
437 if len(raw_line) < 2:
438 self.log.error("Error parsing the recipe in line {}".format(i))
439 raise RecipeExeption
440 cmd = raw_line[1]
441 raw_line = raw_line[0].strip().split(" ")
442 if len(raw_line) == 0:
443 self.log.error("Failed to parse the recipe in line {}".format(i))
444 raise RecipeExeption
445 elif len(raw_line) == 1:
446 if raw_line[0] == "":
447 self.log.error("Failed to parse the recipe in line {}".format(i))
448 raise RecipeExeption
449 machine = raw_line[0]
450 extra = ""
451 elif len(raw_line) == 2:
452 machine = raw_line[0]
453 extra = raw_line[1]
454
455 self._recipe.append((machine.strip(), extra.strip(), cmd.strip()))
456 i = i + 1
5e7f6db7
JS
457
458
5e7f6db7
JS
459class test():
460 def __init__(self, path):
461 self.log = log(4)
462 try:
463 self.path = os.path.abspath(path)
464 except BaseException as e:
465 self.log.error("Could not get absolute path")
466
467 self.log.debug(self.path)
468
469 self.settings_file = "{}/settings".format(self.path)
470 if not os.path.isfile(self.settings_file):
471 self.log.error("No such file: {}".format(self.settings_file))
472
473 self.recipe_file = "{}/recipe".format(self.path)
474 if not os.path.isfile(self.recipe_file):
475 self.log.error("No such file: {}".format(self.recipe_file))
476
477 def read_settings(self):
478 self.config = configparser.ConfigParser()
479 self.config.read(self.settings_file)
480 self.name = self.config["DEFAULT"]["Name"]
481 self.description = self.config["DEFAULT"]["Description"]
482
483 self.virtual_environ_name = self.config["VIRTUAL_ENVIRONMENT"]["Name"]
484 self.virtual_environ_path = self.config["VIRTUAL_ENVIRONMENT"]["Path"]
485 self.virtual_environ_path = os.path.normpath(self.path + "/" + self.virtual_environ_path)
486
487 def virtual_environ_setup(self):
488 self.virtual_environ = virtual_environ(self.virtual_environ_path)
489
490 self.virtual_networks = self.virtual_environ.get_networks()
491
492 self.virtual_machines = self.virtual_environ.get_machines()
493
494 def virtual_environ_start(self):
3fa89b7c
JS
495 for name in self.virtual_environ.network_names:
496 self.virtual_networks[name].define()
497 self.virtual_networks[name].start()
5e7f6db7 498
3fa89b7c
JS
499 for name in self.virtual_environ.machine_names:
500 self.virtual_machines[name].define()
501 self.virtual_machines[name].create_snapshot()
502 self.virtual_machines[name].start()
5e7f6db7 503
3fa89b7c
JS
504 self.log.debug("Try to login on all machines")
505 for name in self.virtual_environ.machine_names:
506 self.virtual_machines[name].login()
5e7f6db7 507
3fa89b7c
JS
508 def load_recipe(self):
509 try:
510 self.recipe = recipe(self.recipe_file)
511 except BaseException:
512 self.log.error("Failed to load recipe")
513 exit(1)
514
515 def run_recipe(self):
516 for line in self.recipe.recipe:
517 return_value = self.virtual_machines[line[0]].cmd(line[2])
518 if not return_value and line[1] == "":
519 self.log.error("Failed to execute command '{}' on {}".format(line[2],line[0]))
520 return False
521 elif return_value == True and line[1] == "!":
522 self.log.error("Succeded to execute command '{}' on {}".format(line[2],line[0]))
523 return False
524
525 def virtual_environ_stop(self):
526 for name in self.virtual_environ.machine_names:
527 self.virtual_machines[name].shutdown()
528 self.virtual_machines[name].revert_snapshot()
529 self.virtual_machines[name].undefine()
530
531 for name in self.virtual_environ.network_names:
532 self.virtual_networks[name].undefine()
5e7f6db7
JS
533
534
5e7f6db7
JS
535# Should return all vms and networks in a list
536# and should provide the path to the necessary xml files
537class virtual_environ():
538 def __init__(self, path):
539 self.log = log(4)
540 try:
541 self.path = os.path.abspath(path)
542 except BaseException as e:
543 self.log.error("Could not get absolute path")
544
545 self.log.debug(self.path)
546
547 self.settings_file = "{}/settings".format(self.path)
548 if not os.path.isfile(self.settings_file):
549 self.log.error("No such file: {}".format(self.settings_file))
550
551 self.log.debug(self.settings_file)
552 self.config = configparser.ConfigParser()
553 self.config.read(self.settings_file)
554 self.name = self.config["DEFAULT"]["name"]
555 self.machines_string = self.config["DEFAULT"]["machines"]
556 self.networks_string = self.config["DEFAULT"]["networks"]
557
558 self.machines = []
559 for machine in self.machines_string.split(","):
560 self.machines.append(machine.strip())
561
562 self.networks = []
563 for network in self.networks_string.split(","):
564 self.networks.append(network.strip())
565
566 self.log.debug(self.machines)
567 self.log.debug(self.networks)
568
569 def get_networks(self):
570 networks = {}
571 for _network in self.networks:
572 self.log.debug(_network)
573 networks.setdefault(_network, network(os.path.normpath(self.path + "/" + self.config[_network]["xml_file"])))
574 return networks
575
576 def get_machines(self):
577 machines = {}
578 for _machine in self.machines:
579 self.log.debug(_machine)
580 machines.setdefault(_machine, vm(
581 os.path.normpath(self.path + "/" + self.config[_machine]["xml_file"]),
2d53bc4f
JS
582 os.path.normpath(self.path + "/" + self.config[_machine]["snapshot_xml_file"]),
583 self.config[_machine]["image"],
584 self.config[_machine]["root_uid"],
585 self.config[_machine]["username"],
586 self.config[_machine]["password"]))
5e7f6db7
JS
587
588 return machines
589
2d53bc4f
JS
590 @property
591 def machine_names(self):
592 return self.machines
593
594 @property
595 def network_names(self):
596 return self.networks
597
5e7f6db7 598
5e7f6db7
JS
599if __name__ == "__main__":
600 import argparse
601
602 parser = argparse.ArgumentParser()
603
604 parser.add_argument("-d", "--directory", dest="dir")
605
606 args = parser.parse_args()
607
5e7f6db7
JS
608 currenttest = test(args.dir)
609 currenttest.read_settings()
10f65154
JS
610 currenttest.virtual_environ_setup()
611 currenttest.load_recipe()
612 currenttest.virtual_environ_start()
613 currenttest.run_recipe()
614 currenttest.virtual_environ_stop()