]>
git.ipfire.org Git - nitsi.git/blob - test.py
11 import xml
.etree
.ElementTree
as ET
20 def __init__(self
, log_level
):
21 self
.log_level
= log_level
23 def debug(self
, string
):
24 if self
.log_level
>= 4:
25 print("DEBUG: {}".format(string
))
27 def error(self
, string
):
28 print("ERROR: {}".format(string
))
31 def __init__(self
, uri
):
34 self
.connection
= None
36 def get_domain_from_name(self
, name
):
37 dom
= self
.con
.lookupByName(name
)
45 if self
.connection
== None:
47 self
.connection
= libvirt
.open(self
.uri
)
48 except BaseException
as error
:
49 self
.log
.error("Could not connect to: {}".format(self
.uri
))
51 self
.log
.debug("Connected to: {}".format(self
.uri
))
52 return self
.connection
54 return self
.connection
58 def __init__(self
, vm_xml_file
, snapshot_xml_file
, image
, root_uid
, username
, password
):
60 self
.con
= libvirt_con("qemu:///system")
62 with
open(vm_xml_file
) as fobj
:
63 self
.vm_xml
= fobj
.read()
64 except FileNotFoundError
as error
:
65 self
.log
.error("No such file: {}".format(vm_xml_file
))
68 with
open(snapshot_xml_file
) as fobj
:
69 self
.snapshot_xml
= fobj
.read()
70 except FileNotFoundError
as error
:
71 self
.log
.error("No such file: {}".format(snapshot_xml_file
))
75 if not os
.path
.isfile(self
.image
):
76 self
.log
.error("No such file: {}".format(self
.image
))
78 self
.root_uid
= root_uid
79 self
.disk
= disk(image
)
81 self
.username
= username
82 self
.password
= password
85 self
.dom
= self
.con
.con
.defineXML(self
.vm_xml
)
87 self
.log
.error("Could not define VM")
91 if self
.dom
.create() < 0:
92 self
.log
.error("Could not start VM")
97 if self
.dom
.shutdown() < 0:
98 self
.log
.error("Could not shutdown VM")
101 self
.log
.error("Domain is not running")
106 def create_snapshot(self
):
108 self
.snapshot
= self
.dom
.snapshotCreateXML(self
.snapshot_xml
)
110 if not self
.snapshot
:
111 self
.log
.error("Could not create snapshot")
114 def revert_snapshot(self
):
115 self
.dom
.revertToSnapshot(self
.snapshot
)
116 self
.snapshot
.delete()
118 def is_running(self
):
120 state
, reason
= self
.dom
.state()
122 if state
== libvirt
.VIR_DOMAIN_RUNNING
:
127 def get_serial_device(self
):
129 if not self
.is_running():
132 xml_root
= ET
.fromstring(self
.dom
.XMLDesc(0))
134 elem
= xml_root
.find("./devices/serial/source")
135 return elem
.get("path")
137 def check_is_booted_up(self
):
138 serial_con
= serial_connection(self
.get_serial_device())
140 serial_con
.write("\n")
141 # This will block till the domain is booted up
148 self
.serial_con
= serial_connection(self
.get_serial_device(), username
=self
.username
)
149 self
.serial_con
.login(self
.password
)
150 except BaseException
as e
:
151 self
.log
.error("Could not connect to the domain via serial console")
154 return self
.serial_con
.command(cmd
)
156 def copy_in(self
, fr
, to
):
158 self
.disk
.mount(self
.root_uid
, "/")
159 self
.disk
.copy_in(fr
, to
)
160 except BaseException
as e
:
163 self
.disk
.umount("/")
166 class serial_connection():
167 def __init__(self
, device
, username
=None):
169 self
.back_at_prompt_pattern
= None
170 self
.username
= username
172 self
.con
= serial
.Serial(device
)
174 def read(self
, size
=1):
175 if len(self
.buffer) >= size
:
176 # throw away first size bytes in buffer
177 data
= self
.buffer[:size
]
178 # Set the buffer to the non used bytes
179 self
.buffer = self
.buffer[size
:]
183 # Set the size to the value we have to read now
184 size
= size
- len(self
.buffer)
185 # Set the buffer empty
187 return data
+ self
.con
.read(size
)
189 def peek(self
, size
=1):
190 if len(self
.buffer) <= size
:
191 self
.buffer += self
.con
.read(size
=size
- len(self
.buffer))
193 return self
.buffer[:size
]
196 self
.log
.debug(self
.buffer)
197 self
.buffer = self
.buffer + self
.con
.read(self
.con
.in_waiting
)
198 if b
"\n" in self
.buffer:
199 size
= self
.buffer.index(b
"\n") + 1
200 self
.log
.debug("We have a whole line in the buffer")
201 self
.log
.debug(self
.buffer)
202 self
.log
.debug("We split at {}".format(size
))
203 data
= self
.buffer[:size
]
204 self
.buffer = self
.buffer[size
:]
206 self
.log
.debug(self
.buffer)
211 return data
+ self
.con
.readline()
213 def back_at_prompt(self
):
218 # We need to use self.in_waiting because with self.con.in_waiting we get
219 # not the complete string
220 size
= len(self
.buffer) + self
.in_waiting
221 data
= self
.peek(size
)
224 if self
.back_at_prompt_pattern
== None:
225 #self.back_at_prompt_pattern = r"^\[{}@.+\]#".format(self.username)
226 self
.back_at_prompt_pattern
= re
.compile(r
"^\[{}@.+\]#".format(self
.username
), re
.MULTILINE
)
228 if self
.back_at_prompt_pattern
.search(data
.decode()):
233 def log_console_line(self
, line
):
234 self
.log
.debug("Get in function log_console_line()")
235 sys
.stdout
.write(line
)
238 def in_waiting(self
):
239 in_waiting_before
= 0
242 while in_waiting_before
!= self
.con
.in_waiting
:
243 in_waiting_before
= self
.con
.in_waiting
246 return self
.con
.in_waiting
248 def line_in_buffer(self
):
249 if b
"\n" in self
.buffer:
254 def print_lines_in_buffer(self
):
256 self
.log
.debug("Fill buffer ...")
257 self
.peek(len(self
.buffer) + self
.in_waiting
)
258 self
.log
.debug("Current buffer length: {}".format(len(self
.buffer)))
259 if self
.line_in_buffer() == True:
260 while self
.line_in_buffer() == True:
261 data
= self
.readline()
262 self
.log_console_line(data
.decode())
264 self
.log
.debug("We have printed all lines in the buffer")
267 def login(self
, password
):
268 if self
.username
== None:
269 self
.log
.error("Username cannot be blank")
272 self
.print_lines_in_buffer()
274 # Hit enter to see what we get
275 self
.con
.write(b
'\n')
276 # We get two new lines \r\n ?
277 data
= self
.readline()
278 self
.log_console_line(data
.decode())
280 self
.print_lines_in_buffer()
282 if self
.back_at_prompt():
283 self
.log
.debug("We are already logged in.")
286 # Read all line till we get login:
288 # We need to use self.in_waiting because with self.con.in_waiting we get
289 # not the complete string
290 size
= len(self
.buffer) + self
.in_waiting
291 data
= self
.peek(size
)
293 pattern
= r
"^.*login: "
294 pattern
= re
.compile(pattern
)
296 if pattern
.search(data
.decode()):
299 self
.log
.debug("The pattern does not match")
300 self
.log_console_line(self
.readline().decode())
303 string
= "{}\n".format(self
.username
)
304 self
.con
.write(string
.encode())
306 # read the login out of the buffer
307 data
= self
.readline()
308 self
.log
.debug("This is the login:{}".format(data
))
309 self
.log_console_line(data
.decode())
311 # We need to wait her till we get the full string "Password:"
312 #This is useless but self.in_waiting will wait the correct amount of time
313 size
= self
.in_waiting
315 string
= "{}\n".format(password
)
316 self
.con
.write(string
.encode())
318 # Print the 'Password:' line
319 data
= self
.readline()
320 self
.log_console_line(data
.decode())
322 while not self
.back_at_prompt():
323 # This will fail if the login failed so we need to look for the failed keyword
324 data
= self
.readline()
325 self
.log_console_line(data
.decode())
329 def write(self
, string
):
330 self
.log
.debug(string
)
331 self
.con
.write(string
.encode())
334 def command(self
, command
):
335 self
.write("{}; echo \"END: $?\"\n".format(command
))
337 # We need to read out the prompt for this command first
338 # If we do not do this we will break the loop immediately
339 # because the prompt for this command is still in the buffer
340 data
= self
.readline()
341 self
.log_console_line(data
.decode())
343 while not self
.back_at_prompt():
344 data
= self
.readline()
345 self
.log_console_line(data
.decode())
347 # We saved our exit code in data (the last line)
348 self
.log
.debug(data
.decode())
349 data
= data
.decode().replace("END: ", "")
351 self
.log
.debug(data
.strip())
355 # A class which define and undefine a virtual network based on an xml file
357 def __init__(self
, network_xml_file
):
359 self
.con
= libvirt_con("qemu:///system")
361 with
open(network_xml_file
) as fobj
:
362 self
.network_xml
= fobj
.read()
363 except FileNotFoundError
as error
:
364 self
.log
.error("No such file: {}".format(vm_xml_file
))
367 self
.network
= self
.con
.con
.networkDefineXML(self
.network_xml
)
370 self
.log
.error("Failed to define virtual network")
373 self
.network
.create()
376 self
.network
.destroy()
380 class RecipeExeption(Exception):
385 # Should read the test, check if the syntax are valid
386 # and return tuples with the ( host, command ) structure
388 def __init__(self
, path
, circle
=[]):
390 self
.recipe_file
= path
391 self
.path
= os
.path
.dirname(self
.recipe_file
)
392 self
.log
.debug("Path of recipe is: {}".format(self
.recipe_file
))
394 self
._machines
= None
396 self
.in_recursion
= True
398 self
.in_recursion
= False
401 self
.log
.debug(circle
)
402 self
.log
.debug(self
.circle
)
404 if not os
.path
.isfile(self
.recipe_file
):
405 self
.log
.error("No such file: {}".format(self
.recipe_file
))
408 with
open(self
.recipe_file
) as fobj
:
409 self
.raw_recipe
= fobj
.readlines()
410 except FileNotFoundError
as error
:
411 self
.log
.error("No such file: {}".format(vm_xml_file
))
422 if not self
._machines
:
424 for line
in self
._recipe
:
425 if line
[0] != "all" and line
[0] not in self
._machines
:
426 self
._machines
.append(line
[0])
428 return self
._machines
433 for line
in self
.raw_recipe
:
434 raw_line
= line
.split(":")
435 if len(raw_line
) < 2:
436 self
.log
.error("Error parsing the recipe in line {}".format(i
))
438 cmd
= raw_line
[1].strip()
439 raw_line
= raw_line
[0].strip().split(" ")
440 if len(raw_line
) == 0:
441 self
.log
.error("Failed to parse the recipe in line {}".format(i
))
444 if raw_line
[0].strip() == "":
445 self
.log
.error("Failed to parse the recipe in line {}".format(i
))
448 machine
= raw_line
[0].strip()
450 if len(raw_line
) == 2:
451 extra
= raw_line
[1].strip()
455 # We could get a machine here or a include statement
456 if machine
== "include":
458 path
= os
.path
.normpath(self
.path
+ "/" + path
)
459 path
= path
+ "/recipe"
460 if path
in self
.circle
:
461 self
.log
.error("Detect import loop!")
463 self
.circle
.append(path
)
464 recipe_to_include
= recipe(path
, circle
=self
.circle
)
466 if machine
== "include":
467 self
._recipe
.extend(recipe_to_include
.recipe
)
469 # Support also something like 'alice,bob: echo'
470 machines
= machine
.split(",")
471 for machine
in machines
:
472 self
._recipe
.append((machine
.strip(), extra
.strip(), cmd
.strip()))
475 if not self
.in_recursion
:
477 for line
in self
._recipe
:
479 tmp_recipe
.append(line
)
481 for machine
in self
.machines
:
482 tmp_recipe
.append((machine
.strip(), line
[1], line
[2]))
484 self
._recipe
= tmp_recipe
489 def __init__(self
, path
):
492 self
.path
= os
.path
.abspath(path
)
493 except BaseException
as e
:
494 self
.log
.error("Could not get absolute path")
496 self
.log
.debug(self
.path
)
498 self
.settings_file
= "{}/settings".format(self
.path
)
499 if not os
.path
.isfile(self
.settings_file
):
500 self
.log
.error("No such file: {}".format(self
.settings_file
))
502 self
.recipe_file
= "{}/recipe".format(self
.path
)
503 if not os
.path
.isfile(self
.recipe_file
):
504 self
.log
.error("No such file: {}".format(self
.recipe_file
))
506 def read_settings(self
):
507 self
.config
= configparser
.ConfigParser()
508 self
.config
.read(self
.settings_file
)
509 self
.name
= self
.config
["DEFAULT"]["Name"]
510 self
.description
= self
.config
["DEFAULT"]["Description"]
511 self
.copy_to
= self
.config
["DEFAULT"]["Copy_to"]
512 self
.copy_from
= self
.config
["DEFAULT"]["Copy_from"]
513 self
.copy_from
= self
.copy_from
.split(",")
516 for file in self
.copy_from
:
518 file = os
.path
.normpath(self
.path
+ "/" + file)
523 self
.virtual_environ_name
= self
.config
["VIRTUAL_ENVIRONMENT"]["Name"]
524 self
.virtual_environ_path
= self
.config
["VIRTUAL_ENVIRONMENT"]["Path"]
525 self
.virtual_environ_path
= os
.path
.normpath(self
.path
+ "/" + self
.virtual_environ_path
)
527 def virtual_environ_setup(self
):
528 self
.virtual_environ
= virtual_environ(self
.virtual_environ_path
)
530 self
.virtual_networks
= self
.virtual_environ
.get_networks()
532 self
.virtual_machines
= self
.virtual_environ
.get_machines()
534 def virtual_environ_start(self
):
535 for name
in self
.virtual_environ
.network_names
:
536 self
.virtual_networks
[name
].define()
537 self
.virtual_networks
[name
].start()
539 for name
in self
.virtual_environ
.machine_names
:
540 self
.virtual_machines
[name
].define()
541 self
.virtual_machines
[name
].create_snapshot()
542 self
.virtual_machines
[name
].copy_in(self
.copy_from
, self
.copy_to
)
543 self
.virtual_machines
[name
].start()
545 self
.log
.debug("Try to login on all machines")
546 for name
in self
.virtual_environ
.machine_names
:
547 self
.virtual_machines
[name
].login()
549 def load_recipe(self
):
551 self
.recipe
= recipe(self
.recipe_file
)
552 except BaseException
:
553 self
.log
.error("Failed to load recipe")
556 def run_recipe(self
):
557 for line
in self
.recipe
.recipe
:
558 return_value
= self
.virtual_machines
[line
[0]].cmd(line
[2])
559 self
.log
.debug("Return value is: {}".format(return_value
))
560 if return_value
!= "0" and line
[1] == "":
561 self
.log
.error("Failed to execute command '{}' on {}, return code: {}".format(line
[2],line
[0], return_value
))
563 elif return_value
== "0" and line
[1] == "!":
564 self
.log
.error("Succeded to execute command '{}' on {}, return code: {}".format(line
[2],line
[0],return_value
))
567 self
.log
.debug("Command '{}' on {} returned with: {}".format(line
[2],line
[0],return_value
))
569 def virtual_environ_stop(self
):
570 for name
in self
.virtual_environ
.machine_names
:
571 self
.virtual_machines
[name
].shutdown()
572 self
.virtual_machines
[name
].revert_snapshot()
573 self
.virtual_machines
[name
].undefine()
575 for name
in self
.virtual_environ
.network_names
:
576 self
.virtual_networks
[name
].undefine()
579 # Should return all vms and networks in a list
580 # and should provide the path to the necessary xml files
581 class virtual_environ():
582 def __init__(self
, path
):
585 self
.path
= os
.path
.abspath(path
)
586 except BaseException
as e
:
587 self
.log
.error("Could not get absolute path")
589 self
.log
.debug(self
.path
)
591 self
.settings_file
= "{}/settings".format(self
.path
)
592 if not os
.path
.isfile(self
.settings_file
):
593 self
.log
.error("No such file: {}".format(self
.settings_file
))
595 self
.log
.debug(self
.settings_file
)
596 self
.config
= configparser
.ConfigParser()
597 self
.config
.read(self
.settings_file
)
598 self
.name
= self
.config
["DEFAULT"]["name"]
599 self
.machines_string
= self
.config
["DEFAULT"]["machines"]
600 self
.networks_string
= self
.config
["DEFAULT"]["networks"]
603 for machine
in self
.machines_string
.split(","):
604 self
.machines
.append(machine
.strip())
607 for network
in self
.networks_string
.split(","):
608 self
.networks
.append(network
.strip())
610 self
.log
.debug(self
.machines
)
611 self
.log
.debug(self
.networks
)
613 def get_networks(self
):
615 for _network
in self
.networks
:
616 self
.log
.debug(_network
)
617 networks
.setdefault(_network
, network(os
.path
.normpath(self
.path
+ "/" + self
.config
[_network
]["xml_file"])))
620 def get_machines(self
):
622 for _machine
in self
.machines
:
623 self
.log
.debug(_machine
)
624 machines
.setdefault(_machine
, machine(
625 os
.path
.normpath(self
.path
+ "/" + self
.config
[_machine
]["xml_file"]),
626 os
.path
.normpath(self
.path
+ "/" + self
.config
[_machine
]["snapshot_xml_file"]),
627 self
.config
[_machine
]["image"],
628 self
.config
[_machine
]["root_uid"],
629 self
.config
[_machine
]["username"],
630 self
.config
[_machine
]["password"]))
635 def machine_names(self
):
639 def network_names(self
):
643 if __name__
== "__main__":
646 parser
= argparse
.ArgumentParser()
648 parser
.add_argument("-d", "--directory", dest
="dir")
650 args
= parser
.parse_args()
652 currenttest
= test(args
.dir)
653 currenttest
.read_settings()
654 currenttest
.virtual_environ_setup()
655 currenttest
.load_recipe()
657 currenttest
.virtual_environ_start()
658 currenttest
.run_recipe()
659 except BaseException
as e
:
662 currenttest
.virtual_environ_stop()