]>
git.ipfire.org Git - people/ms/nitsi.git/blob - test.py
11 import xml
.etree
.ElementTree
as ET
18 def __init__(self
, log_level
):
19 self
.log_level
= log_level
21 def debug(self
, string
):
22 if self
.log_level
>= 4:
23 print("DEBUG: {}".format(string
))
25 def error(self
, string
):
26 print("ERROR: {}".format(string
))
29 def __init__(self
, uri
):
32 self
.connection
= None
34 def get_domain_from_name(self
, name
):
35 dom
= self
.con
.lookupByName(name
)
43 if self
.connection
== None:
45 self
.connection
= libvirt
.open(self
.uri
)
46 except BaseException
as error
:
47 self
.log
.error("Could not connect to: {}".format(self
.uri
))
49 self
.log
.debug("Connected to: {}".format(self
.uri
))
50 return self
.connection
52 return self
.connection
56 def __init__(self
, vm_xml_file
, snapshot_xml_file
, image
, root_uid
, username
, password
):
58 self
.con
= libvirt_con("qemu:///system")
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
))
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
))
73 if not os
.path
.isfile(self
.image
):
74 self
.log
.error("No such file: {}".format(self
.image
))
76 self
.root_uid
= root_uid
78 self
.username
= username
79 self
.password
= password
82 self
.dom
= self
.con
.con
.defineXML(self
.vm_xml
)
84 self
.log
.error("Could not define VM")
88 if self
.dom
.create() < 0:
89 self
.log
.error("Could not start VM")
94 if self
.dom
.shutdown() < 0:
95 self
.log
.error("Could not shutdown VM")
98 self
.log
.error("Domain is not running")
103 def create_snapshot(self
):
105 self
.snapshot
= self
.dom
.snapshotCreateXML(self
.snapshot_xml
)
107 if not self
.snapshot
:
108 self
.log
.error("Could not create snapshot")
111 def revert_snapshot(self
):
112 self
.dom
.revertToSnapshot(self
.snapshot
)
113 self
.snapshot
.delete()
115 def is_running(self
):
117 state
, reason
= self
.dom
.state()
119 if state
== libvirt
.VIR_DOMAIN_RUNNING
:
124 def get_serial_device(self
):
126 if not self
.is_running():
129 xml_root
= ET
.fromstring(self
.dom
.XMLDesc(0))
131 elem
= xml_root
.find("./devices/serial/source")
132 return elem
.get("path")
134 def check_is_booted_up(self
):
135 serial_con
= connection(self
.get_serial_device())
137 serial_con
.write("\n")
138 # This will block till the domain is booted up
145 self
.serial_con
= connection(self
.get_serial_device(), username
=self
.username
)
146 self
.serial_con
.login(self
.password
)
147 except BaseException
as e
:
148 self
.log
.error("Could not connect to the domain via serial console")
151 return self
.serial_con
.command(cmd
)
155 def __init__(self
, device
, username
=None):
157 self
.back_at_prompt_pattern
= None
158 self
.username
= username
160 self
.con
= serial
.Serial(device
)
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
:]
171 # Set the size to the value we have to read now
172 size
= size
- len(self
.buffer)
173 # Set the buffer empty
175 return data
+ self
.con
.read(size
)
177 def peek(self
, size
=1):
178 if len(self
.buffer) <= size
:
179 self
.buffer += self
.con
.read(size
=size
- len(self
.buffer))
181 return self
.buffer[:size
]
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
:]
194 self
.log
.debug(self
.buffer)
199 return data
+ self
.con
.readline()
201 def back_at_prompt(self
):
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
)
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
)
216 if self
.back_at_prompt_pattern
.search(data
.decode()):
221 def log_console_line(self
, line
):
222 self
.log
.debug("Get in function log_console_line()")
223 sys
.stdout
.write(line
)
226 def in_waiting(self
):
227 in_waiting_before
= 0
230 while in_waiting_before
!= self
.con
.in_waiting
:
231 in_waiting_before
= self
.con
.in_waiting
234 return self
.con
.in_waiting
236 def line_in_buffer(self
):
237 if b
"\n" in self
.buffer:
242 def readline2(self
, pattern
=None):
246 pattern
= re
.compile(pattern
)
249 char
= self
.con
.read(1)
250 string
= string
+ char
.decode("utf-8")
251 string2
= string2
+ char
253 print(char
.decode("utf-8"), end
="")
256 if pattern
and pattern
.match(string
):
259 return {"string" : string
, "return-code" : 1}
265 return {"return-code" : 0}
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")
274 print("We are not logged in")
277 def print_lines_in_buffer(self
):
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())
287 self
.log
.debug("We have printed all lines in the buffer")
290 def login(self
, password
):
291 if self
.username
== None:
292 self
.log
.error("Username cannot be blank")
295 self
.print_lines_in_buffer()
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())
303 self
.print_lines_in_buffer()
305 if self
.back_at_prompt():
306 self
.log
.debug("We are already logged in.")
309 # Read all line till we get login:
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
)
316 pattern
= r
"^.*login: "
317 pattern
= re
.compile(pattern
)
319 if pattern
.search(data
.decode()):
322 self
.log
.debug("The pattern does not match")
323 self
.log_console_line(self
.readline().decode())
326 string
= "{}\n".format(self
.username
)
327 self
.con
.write(string
.encode())
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())
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
338 string
= "{}\n".format(password
)
339 self
.con
.write(string
.encode())
341 # Print the 'Password:' line
342 data
= self
.readline()
343 self
.log_console_line(data
.decode())
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())
352 def write(self
, string
):
353 self
.log
.debug(string
)
354 self
.con
.write(string
.encode())
357 def command(self
, command
):
358 self
.write("{}; echo \"END: $?\"\n".format(command
))
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())
366 while not self
.back_at_prompt():
367 data
= self
.readline()
368 self
.log_console_line(data
.decode())
370 # We saved our exit code in data (the last line)
371 self
.log
.debug(data
.decode())
372 data
= data
.decode().replace("END: ", "")
374 self
.log
.debug(data
.strip())
378 # A class which define and undefine a virtual network based on an xml file
380 def __init__(self
, network_xml_file
):
382 self
.con
= libvirt_con("qemu:///system")
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
))
390 self
.network
= self
.con
.con
.networkDefineXML(self
.network_xml
)
393 self
.log
.error("Failed to define virtual network")
396 self
.network
.create()
399 self
.network
.destroy()
403 class RecipeExeption(Exception):
408 # Should read the test, check if the syntax are valid
409 # and return tuples with the ( host, command ) structure
411 def __init__(self
, path
):
413 self
.recipe_file
= path
416 if not os
.path
.isfile(self
.recipe_file
):
417 self
.log
.error("No such file: {}".format(self
.recipe_file
))
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
))
435 for line
in self
.raw_recipe
:
436 raw_line
= line
.split(":")
437 if len(raw_line
) < 2:
438 self
.log
.error("Error parsing the recipe in line {}".format(i
))
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
))
445 elif len(raw_line
) == 1:
446 if raw_line
[0] == "":
447 self
.log
.error("Failed to parse the recipe in line {}".format(i
))
449 machine
= raw_line
[0]
451 elif len(raw_line
) == 2:
452 machine
= raw_line
[0]
455 self
._recipe
.append((machine
.strip(), extra
.strip(), cmd
.strip()))
460 def __init__(self
, path
):
463 self
.path
= os
.path
.abspath(path
)
464 except BaseException
as e
:
465 self
.log
.error("Could not get absolute path")
467 self
.log
.debug(self
.path
)
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
))
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
))
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"]
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
)
487 def virtual_environ_setup(self
):
488 self
.virtual_environ
= virtual_environ(self
.virtual_environ_path
)
490 self
.virtual_networks
= self
.virtual_environ
.get_networks()
492 self
.virtual_machines
= self
.virtual_environ
.get_machines()
494 def virtual_environ_start(self
):
495 for name
in self
.virtual_environ
.network_names
:
496 self
.virtual_networks
[name
].define()
497 self
.virtual_networks
[name
].start()
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()
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()
508 def load_recipe(self
):
510 self
.recipe
= recipe(self
.recipe_file
)
511 except BaseException
:
512 self
.log
.error("Failed to load recipe")
515 def run_recipe(self
):
516 for line
in self
.recipe
.recipe
:
517 return_value
= self
.virtual_machines
[line
[0]].cmd(line
[2])
518 self
.log
.debug("Return value is: {}".format(return_value
))
519 if return_value
!= "0" and line
[1] == "":
520 self
.log
.error("Failed to execute command '{}' on {}, return code: {}".format(line
[2],line
[0], return_value
))
522 elif return_value
== "0" and line
[1] == "!":
523 self
.log
.error("Succeded to execute command '{}' on {}, return code: {}".format(line
[2],line
[0],return_value
))
526 self
.log
.debug("Command '{}' on {} returned with: {}".format(line
[2],line
[0],return_value
))
528 def virtual_environ_stop(self
):
529 for name
in self
.virtual_environ
.machine_names
:
530 self
.virtual_machines
[name
].shutdown()
531 self
.virtual_machines
[name
].revert_snapshot()
532 self
.virtual_machines
[name
].undefine()
534 for name
in self
.virtual_environ
.network_names
:
535 self
.virtual_networks
[name
].undefine()
538 # Should return all vms and networks in a list
539 # and should provide the path to the necessary xml files
540 class virtual_environ():
541 def __init__(self
, path
):
544 self
.path
= os
.path
.abspath(path
)
545 except BaseException
as e
:
546 self
.log
.error("Could not get absolute path")
548 self
.log
.debug(self
.path
)
550 self
.settings_file
= "{}/settings".format(self
.path
)
551 if not os
.path
.isfile(self
.settings_file
):
552 self
.log
.error("No such file: {}".format(self
.settings_file
))
554 self
.log
.debug(self
.settings_file
)
555 self
.config
= configparser
.ConfigParser()
556 self
.config
.read(self
.settings_file
)
557 self
.name
= self
.config
["DEFAULT"]["name"]
558 self
.machines_string
= self
.config
["DEFAULT"]["machines"]
559 self
.networks_string
= self
.config
["DEFAULT"]["networks"]
562 for machine
in self
.machines_string
.split(","):
563 self
.machines
.append(machine
.strip())
566 for network
in self
.networks_string
.split(","):
567 self
.networks
.append(network
.strip())
569 self
.log
.debug(self
.machines
)
570 self
.log
.debug(self
.networks
)
572 def get_networks(self
):
574 for _network
in self
.networks
:
575 self
.log
.debug(_network
)
576 networks
.setdefault(_network
, network(os
.path
.normpath(self
.path
+ "/" + self
.config
[_network
]["xml_file"])))
579 def get_machines(self
):
581 for _machine
in self
.machines
:
582 self
.log
.debug(_machine
)
583 machines
.setdefault(_machine
, vm(
584 os
.path
.normpath(self
.path
+ "/" + self
.config
[_machine
]["xml_file"]),
585 os
.path
.normpath(self
.path
+ "/" + self
.config
[_machine
]["snapshot_xml_file"]),
586 self
.config
[_machine
]["image"],
587 self
.config
[_machine
]["root_uid"],
588 self
.config
[_machine
]["username"],
589 self
.config
[_machine
]["password"]))
594 def machine_names(self
):
598 def network_names(self
):
602 if __name__
== "__main__":
605 parser
= argparse
.ArgumentParser()
607 parser
.add_argument("-d", "--directory", dest
="dir")
609 args
= parser
.parse_args()
611 currenttest
= test(args
.dir)
612 currenttest
.read_settings()
613 currenttest
.virtual_environ_setup()
614 currenttest
.load_recipe()
615 currenttest
.virtual_environ_start()
616 currenttest
.run_recipe()
617 currenttest
.virtual_environ_stop()