]>
git.ipfire.org Git - people/ms/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
= connection(self
.get_serial_device())
140 serial_con
.write("\n")
141 # This will block till the domain is booted up
148 self
.serial_con
= 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("/")
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 readline2(self
, pattern
=None):
258 pattern
= re
.compile(pattern
)
261 char
= self
.con
.read(1)
262 string
= string
+ char
.decode("utf-8")
263 string2
= string2
+ char
265 print(char
.decode("utf-8"), end
="")
268 if pattern
and pattern
.match(string
):
271 return {"string" : string
, "return-code" : 1}
277 return {"return-code" : 0}
279 def check_logged_in(self
, username
):
280 pattern
= "^\[" + username
+ "@.+\]#"
281 data
= self
.readline(pattern
=pattern
)
282 if data
["return-code"] == 1:
283 print("We are logged in")
286 print("We are not logged in")
289 def print_lines_in_buffer(self
):
291 self
.log
.debug("Fill buffer ...")
292 self
.peek(len(self
.buffer) + self
.in_waiting
)
293 self
.log
.debug("Current buffer length: {}".format(len(self
.buffer)))
294 if self
.line_in_buffer() == True:
295 while self
.line_in_buffer() == True:
296 data
= self
.readline()
297 self
.log_console_line(data
.decode())
299 self
.log
.debug("We have printed all lines in the buffer")
302 def login(self
, password
):
303 if self
.username
== None:
304 self
.log
.error("Username cannot be blank")
307 self
.print_lines_in_buffer()
309 # Hit enter to see what we get
310 self
.con
.write(b
'\n')
311 # We get two new lines \r\n ?
312 data
= self
.readline()
313 self
.log_console_line(data
.decode())
315 self
.print_lines_in_buffer()
317 if self
.back_at_prompt():
318 self
.log
.debug("We are already logged in.")
321 # Read all line till we get login:
323 # We need to use self.in_waiting because with self.con.in_waiting we get
324 # not the complete string
325 size
= len(self
.buffer) + self
.in_waiting
326 data
= self
.peek(size
)
328 pattern
= r
"^.*login: "
329 pattern
= re
.compile(pattern
)
331 if pattern
.search(data
.decode()):
334 self
.log
.debug("The pattern does not match")
335 self
.log_console_line(self
.readline().decode())
338 string
= "{}\n".format(self
.username
)
339 self
.con
.write(string
.encode())
341 # read the login out of the buffer
342 data
= self
.readline()
343 self
.log
.debug("This is the login:{}".format(data
))
344 self
.log_console_line(data
.decode())
346 # We need to wait her till we get the full string "Password:"
347 #This is useless but self.in_waiting will wait the correct amount of time
348 size
= self
.in_waiting
350 string
= "{}\n".format(password
)
351 self
.con
.write(string
.encode())
353 # Print the 'Password:' line
354 data
= self
.readline()
355 self
.log_console_line(data
.decode())
357 while not self
.back_at_prompt():
358 # This will fail if the login failed so we need to look for the failed keyword
359 data
= self
.readline()
360 self
.log_console_line(data
.decode())
364 def write(self
, string
):
365 self
.log
.debug(string
)
366 self
.con
.write(string
.encode())
369 def command(self
, command
):
370 self
.write("{}; echo \"END: $?\"\n".format(command
))
372 # We need to read out the prompt for this command first
373 # If we do not do this we will break the loop immediately
374 # because the prompt for this command is still in the buffer
375 data
= self
.readline()
376 self
.log_console_line(data
.decode())
378 while not self
.back_at_prompt():
379 data
= self
.readline()
380 self
.log_console_line(data
.decode())
382 # We saved our exit code in data (the last line)
383 self
.log
.debug(data
.decode())
384 data
= data
.decode().replace("END: ", "")
386 self
.log
.debug(data
.strip())
390 # A class which define and undefine a virtual network based on an xml file
392 def __init__(self
, network_xml_file
):
394 self
.con
= libvirt_con("qemu:///system")
396 with
open(network_xml_file
) as fobj
:
397 self
.network_xml
= fobj
.read()
398 except FileNotFoundError
as error
:
399 self
.log
.error("No such file: {}".format(vm_xml_file
))
402 self
.network
= self
.con
.con
.networkDefineXML(self
.network_xml
)
405 self
.log
.error("Failed to define virtual network")
408 self
.network
.create()
411 self
.network
.destroy()
415 class RecipeExeption(Exception):
420 # Should read the test, check if the syntax are valid
421 # and return tuples with the ( host, command ) structure
423 def __init__(self
, path
, circle
=[]):
425 self
.recipe_file
= path
426 self
.path
= os
.path
.dirname(self
.recipe_file
)
427 self
.log
.debug("Path of recipe is: {}".format(self
.recipe_file
))
429 self
._machines
= None
431 self
.in_recursion
= True
433 self
.in_recursion
= False
436 self
.log
.debug(circle
)
437 self
.log
.debug(self
.circle
)
439 if not os
.path
.isfile(self
.recipe_file
):
440 self
.log
.error("No such file: {}".format(self
.recipe_file
))
443 with
open(self
.recipe_file
) as fobj
:
444 self
.raw_recipe
= fobj
.readlines()
445 except FileNotFoundError
as error
:
446 self
.log
.error("No such file: {}".format(vm_xml_file
))
457 if not self
._machines
:
459 for line
in self
._recipe
:
460 if line
[0] != "all" and line
[0] not in self
._machines
:
461 self
._machines
.append(line
[0])
463 return self
._machines
468 for line
in self
.raw_recipe
:
469 raw_line
= line
.split(":")
470 if len(raw_line
) < 2:
471 self
.log
.error("Error parsing the recipe in line {}".format(i
))
473 cmd
= raw_line
[1].strip()
474 raw_line
= raw_line
[0].strip().split(" ")
475 if len(raw_line
) == 0:
476 self
.log
.error("Failed to parse the recipe in line {}".format(i
))
479 if raw_line
[0].strip() == "":
480 self
.log
.error("Failed to parse the recipe in line {}".format(i
))
483 machine
= raw_line
[0].strip()
485 if len(raw_line
) == 2:
486 extra
= raw_line
[1].strip()
490 # We could get a machine here or a include statement
491 if machine
== "include":
493 path
= os
.path
.normpath(self
.path
+ "/" + path
)
494 path
= path
+ "/recipe"
495 if path
in self
.circle
:
496 self
.log
.error("Detect import loop!")
498 self
.circle
.append(path
)
499 recipe_to_include
= recipe(path
, circle
=self
.circle
)
501 if machine
== "include":
502 self
._recipe
.extend(recipe_to_include
.recipe
)
504 # Support also something like 'alice,bob: echo'
505 machines
= machine
.split(",")
506 for machine
in machines
:
507 self
._recipe
.append((machine
.strip(), extra
.strip(), cmd
.strip()))
510 if not self
.in_recursion
:
512 for line
in self
._recipe
:
514 tmp_recipe
.append(line
)
516 for machine
in self
.machines
:
517 tmp_recipe
.append((machine
.strip(), line
[1], line
[2]))
519 self
._recipe
= tmp_recipe
524 def __init__(self
, path
):
527 self
.path
= os
.path
.abspath(path
)
528 except BaseException
as e
:
529 self
.log
.error("Could not get absolute path")
531 self
.log
.debug(self
.path
)
533 self
.settings_file
= "{}/settings".format(self
.path
)
534 if not os
.path
.isfile(self
.settings_file
):
535 self
.log
.error("No such file: {}".format(self
.settings_file
))
537 self
.recipe_file
= "{}/recipe".format(self
.path
)
538 if not os
.path
.isfile(self
.recipe_file
):
539 self
.log
.error("No such file: {}".format(self
.recipe_file
))
541 def read_settings(self
):
542 self
.config
= configparser
.ConfigParser()
543 self
.config
.read(self
.settings_file
)
544 self
.name
= self
.config
["DEFAULT"]["Name"]
545 self
.description
= self
.config
["DEFAULT"]["Description"]
546 self
.copy_to
= self
.config
["DEFAULT"]["Copy_to"]
547 self
.copy_from
= self
.config
["DEFAULT"]["Copy_from"]
548 self
.copy_from
= self
.copy_from
.split(",")
551 for file in self
.copy_from
:
553 file = os
.path
.normpath(self
.path
+ "/" + file)
558 self
.virtual_environ_name
= self
.config
["VIRTUAL_ENVIRONMENT"]["Name"]
559 self
.virtual_environ_path
= self
.config
["VIRTUAL_ENVIRONMENT"]["Path"]
560 self
.virtual_environ_path
= os
.path
.normpath(self
.path
+ "/" + self
.virtual_environ_path
)
562 def virtual_environ_setup(self
):
563 self
.virtual_environ
= virtual_environ(self
.virtual_environ_path
)
565 self
.virtual_networks
= self
.virtual_environ
.get_networks()
567 self
.virtual_machines
= self
.virtual_environ
.get_machines()
569 def virtual_environ_start(self
):
570 for name
in self
.virtual_environ
.network_names
:
571 self
.virtual_networks
[name
].define()
572 self
.virtual_networks
[name
].start()
574 for name
in self
.virtual_environ
.machine_names
:
575 self
.virtual_machines
[name
].define()
576 self
.virtual_machines
[name
].create_snapshot()
577 self
.virtual_machines
[name
].copy_in(self
.copy_from
, self
.copy_to
)
578 self
.virtual_machines
[name
].start()
580 self
.log
.debug("Try to login on all machines")
581 for name
in self
.virtual_environ
.machine_names
:
582 self
.virtual_machines
[name
].login()
584 def load_recipe(self
):
586 self
.recipe
= recipe(self
.recipe_file
)
587 except BaseException
:
588 self
.log
.error("Failed to load recipe")
591 def run_recipe(self
):
592 for line
in self
.recipe
.recipe
:
593 return_value
= self
.virtual_machines
[line
[0]].cmd(line
[2])
594 self
.log
.debug("Return value is: {}".format(return_value
))
595 if return_value
!= "0" and line
[1] == "":
596 self
.log
.error("Failed to execute command '{}' on {}, return code: {}".format(line
[2],line
[0], return_value
))
598 elif return_value
== "0" and line
[1] == "!":
599 self
.log
.error("Succeded to execute command '{}' on {}, return code: {}".format(line
[2],line
[0],return_value
))
602 self
.log
.debug("Command '{}' on {} returned with: {}".format(line
[2],line
[0],return_value
))
604 def virtual_environ_stop(self
):
605 for name
in self
.virtual_environ
.machine_names
:
606 self
.virtual_machines
[name
].shutdown()
607 self
.virtual_machines
[name
].revert_snapshot()
608 self
.virtual_machines
[name
].undefine()
610 for name
in self
.virtual_environ
.network_names
:
611 self
.virtual_networks
[name
].undefine()
614 # Should return all vms and networks in a list
615 # and should provide the path to the necessary xml files
616 class virtual_environ():
617 def __init__(self
, path
):
620 self
.path
= os
.path
.abspath(path
)
621 except BaseException
as e
:
622 self
.log
.error("Could not get absolute path")
624 self
.log
.debug(self
.path
)
626 self
.settings_file
= "{}/settings".format(self
.path
)
627 if not os
.path
.isfile(self
.settings_file
):
628 self
.log
.error("No such file: {}".format(self
.settings_file
))
630 self
.log
.debug(self
.settings_file
)
631 self
.config
= configparser
.ConfigParser()
632 self
.config
.read(self
.settings_file
)
633 self
.name
= self
.config
["DEFAULT"]["name"]
634 self
.machines_string
= self
.config
["DEFAULT"]["machines"]
635 self
.networks_string
= self
.config
["DEFAULT"]["networks"]
638 for machine
in self
.machines_string
.split(","):
639 self
.machines
.append(machine
.strip())
642 for network
in self
.networks_string
.split(","):
643 self
.networks
.append(network
.strip())
645 self
.log
.debug(self
.machines
)
646 self
.log
.debug(self
.networks
)
648 def get_networks(self
):
650 for _network
in self
.networks
:
651 self
.log
.debug(_network
)
652 networks
.setdefault(_network
, network(os
.path
.normpath(self
.path
+ "/" + self
.config
[_network
]["xml_file"])))
655 def get_machines(self
):
657 for _machine
in self
.machines
:
658 self
.log
.debug(_machine
)
659 machines
.setdefault(_machine
, vm(
660 os
.path
.normpath(self
.path
+ "/" + self
.config
[_machine
]["xml_file"]),
661 os
.path
.normpath(self
.path
+ "/" + self
.config
[_machine
]["snapshot_xml_file"]),
662 self
.config
[_machine
]["image"],
663 self
.config
[_machine
]["root_uid"],
664 self
.config
[_machine
]["username"],
665 self
.config
[_machine
]["password"]))
670 def machine_names(self
):
674 def network_names(self
):
678 if __name__
== "__main__":
681 parser
= argparse
.ArgumentParser()
683 parser
.add_argument("-d", "--directory", dest
="dir")
685 args
= parser
.parse_args()
687 currenttest
= test(args
.dir)
688 currenttest
.read_settings()
689 currenttest
.virtual_environ_setup()
690 currenttest
.load_recipe()
692 currenttest
.virtual_environ_start()
693 currenttest
.run_recipe()
694 except BaseException
as e
:
697 currenttest
.virtual_environ_stop()