]>
git.ipfire.org Git - 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 login(self
, password
):
278 if self
.username
== None:
279 self
.log
.error("Username cannot be blank")
282 # Hit enter to see what we get
283 self
.con
.write(b
'\n')
284 # We get two new lines \r\n ?
285 data
= self
.readline()
286 self
.log_console_line(data
.decode())
289 if self
.back_at_prompt():
290 self
.log
.debug("We are already logged in.")
293 # Read all line till we get login:
296 if not data
.decode() == "l":
297 self
.log
.debug("We get no l at the start")
298 self
.log_console_line(self
.readline().decode())
300 # We need to use self.in_waiting because with self.con.in_waiting we get
301 # not the complete string
302 size
= len(self
.buffer) + self
.in_waiting
303 data
= self
.peek(size
)
305 pattern
= r
"^.*login: "
306 pattern
= re
.compile(pattern
)
308 if pattern
.search(data
.decode()):
311 self
.log
.debug("The pattern does not match")
312 self
.log_console_line(self
.readline().decode())
315 string
= "{}\n".format(self
.username
)
316 self
.con
.write(string
.encode())
318 # read the login out of the buffer
319 data
= self
.readline()
320 self
.log
.debug("This is the login:{}".format(data
))
321 self
.log_console_line(data
.decode())
323 # We need to wait her till we get the full string "Password:"
324 #This is useless but self.in_waiting will wait the correct amount of time
325 size
= self
.in_waiting
327 string
= "{}\n".format(password
)
328 self
.con
.write(string
.encode())
330 # Print the 'Password:' line
331 data
= self
.readline()
332 self
.log_console_line(data
.decode())
334 while not self
.back_at_prompt():
335 # This will fail if the login failed so we need to look for the failed keyword
336 data
= self
.readline()
337 self
.log_console_line(data
.decode())
341 def write(self
, string
):
342 self
.log
.debug(string
)
343 self
.con
.write(string
.encode())
346 def command(self
, command
):
347 self
.write("{}\n".format(command
))
349 # We need to read out the prompt for this command first
350 # If we do not do this we will break the loop immediately
351 # because the prompt for this command is still in the buffer
352 data
= self
.readline()
353 self
.log_console_line(data
.decode())
355 while not self
.back_at_prompt():
356 data
= self
.readline()
357 self
.log_console_line(data
.decode())
360 # A class which define and undefine a virtual network based on an xml file
362 def __init__(self
, network_xml_file
):
364 self
.con
= libvirt_con("qemu:///system")
366 with
open(network_xml_file
) as fobj
:
367 self
.network_xml
= fobj
.read()
368 except FileNotFoundError
as error
:
369 self
.log
.error("No such file: {}".format(vm_xml_file
))
372 self
.network
= self
.con
.con
.networkDefineXML(self
.network_xml
)
375 self
.log
.error("Failed to define virtual network")
378 self
.network
.create()
381 self
.network
.destroy()
385 class RecipeExeption(Exception):
390 # Should read the test, check if the syntax are valid
391 # and return tuples with the ( host, command ) structure
393 def __init__(self
, path
):
395 self
.recipe_file
= path
398 if not os
.path
.isfile(self
.recipe_file
):
399 self
.log
.error("No such file: {}".format(self
.recipe_file
))
402 with
open(self
.recipe_file
) as fobj
:
403 self
.raw_recipe
= fobj
.readlines()
404 except FileNotFoundError
as error
:
405 self
.log
.error("No such file: {}".format(vm_xml_file
))
417 for line
in self
.raw_recipe
:
418 raw_line
= line
.split(":")
419 if len(raw_line
) < 2:
420 self
.log
.error("Error parsing the recipe in line {}".format(i
))
423 raw_line
= raw_line
[0].strip().split(" ")
424 if len(raw_line
) == 0:
425 self
.log
.error("Failed to parse the recipe in line {}".format(i
))
427 elif len(raw_line
) == 1:
428 if raw_line
[0] == "":
429 self
.log
.error("Failed to parse the recipe in line {}".format(i
))
431 machine
= raw_line
[0]
433 elif len(raw_line
) == 2:
434 machine
= raw_line
[0]
437 self
._recipe
.append((machine
.strip(), extra
.strip(), cmd
.strip()))
442 def __init__(self
, path
):
445 self
.path
= os
.path
.abspath(path
)
446 except BaseException
as e
:
447 self
.log
.error("Could not get absolute path")
449 self
.log
.debug(self
.path
)
451 self
.settings_file
= "{}/settings".format(self
.path
)
452 if not os
.path
.isfile(self
.settings_file
):
453 self
.log
.error("No such file: {}".format(self
.settings_file
))
455 self
.recipe_file
= "{}/recipe".format(self
.path
)
456 if not os
.path
.isfile(self
.recipe_file
):
457 self
.log
.error("No such file: {}".format(self
.recipe_file
))
459 def read_settings(self
):
460 self
.config
= configparser
.ConfigParser()
461 self
.config
.read(self
.settings_file
)
462 self
.name
= self
.config
["DEFAULT"]["Name"]
463 self
.description
= self
.config
["DEFAULT"]["Description"]
465 self
.virtual_environ_name
= self
.config
["VIRTUAL_ENVIRONMENT"]["Name"]
466 self
.virtual_environ_path
= self
.config
["VIRTUAL_ENVIRONMENT"]["Path"]
467 self
.virtual_environ_path
= os
.path
.normpath(self
.path
+ "/" + self
.virtual_environ_path
)
469 def virtual_environ_setup(self
):
470 self
.virtual_environ
= virtual_environ(self
.virtual_environ_path
)
472 self
.virtual_networks
= self
.virtual_environ
.get_networks()
474 self
.virtual_machines
= self
.virtual_environ
.get_machines()
476 def virtual_environ_start(self
):
477 for name
in self
.virtual_environ
.network_names
:
478 self
.virtual_networks
[name
].define()
479 self
.virtual_networks
[name
].start()
481 for name
in self
.virtual_environ
.machine_names
:
482 self
.virtual_machines
[name
].define()
483 self
.virtual_machines
[name
].create_snapshot()
484 self
.virtual_machines
[name
].start()
486 self
.log
.debug("Try to login on all machines")
487 for name
in self
.virtual_environ
.machine_names
:
488 self
.virtual_machines
[name
].login()
490 def load_recipe(self
):
492 self
.recipe
= recipe(self
.recipe_file
)
493 except BaseException
:
494 self
.log
.error("Failed to load recipe")
497 def run_recipe(self
):
498 for line
in self
.recipe
.recipe
:
499 return_value
= self
.virtual_machines
[line
[0]].cmd(line
[2])
500 if not return_value
and line
[1] == "":
501 self
.log
.error("Failed to execute command '{}' on {}".format(line
[2],line
[0]))
503 elif return_value
== True and line
[1] == "!":
504 self
.log
.error("Succeded to execute command '{}' on {}".format(line
[2],line
[0]))
507 def virtual_environ_stop(self
):
508 for name
in self
.virtual_environ
.machine_names
:
509 self
.virtual_machines
[name
].shutdown()
510 self
.virtual_machines
[name
].revert_snapshot()
511 self
.virtual_machines
[name
].undefine()
513 for name
in self
.virtual_environ
.network_names
:
514 self
.virtual_networks
[name
].undefine()
517 # Should return all vms and networks in a list
518 # and should provide the path to the necessary xml files
519 class virtual_environ():
520 def __init__(self
, path
):
523 self
.path
= os
.path
.abspath(path
)
524 except BaseException
as e
:
525 self
.log
.error("Could not get absolute path")
527 self
.log
.debug(self
.path
)
529 self
.settings_file
= "{}/settings".format(self
.path
)
530 if not os
.path
.isfile(self
.settings_file
):
531 self
.log
.error("No such file: {}".format(self
.settings_file
))
533 self
.log
.debug(self
.settings_file
)
534 self
.config
= configparser
.ConfigParser()
535 self
.config
.read(self
.settings_file
)
536 self
.name
= self
.config
["DEFAULT"]["name"]
537 self
.machines_string
= self
.config
["DEFAULT"]["machines"]
538 self
.networks_string
= self
.config
["DEFAULT"]["networks"]
541 for machine
in self
.machines_string
.split(","):
542 self
.machines
.append(machine
.strip())
545 for network
in self
.networks_string
.split(","):
546 self
.networks
.append(network
.strip())
548 self
.log
.debug(self
.machines
)
549 self
.log
.debug(self
.networks
)
551 def get_networks(self
):
553 for _network
in self
.networks
:
554 self
.log
.debug(_network
)
555 networks
.setdefault(_network
, network(os
.path
.normpath(self
.path
+ "/" + self
.config
[_network
]["xml_file"])))
558 def get_machines(self
):
560 for _machine
in self
.machines
:
561 self
.log
.debug(_machine
)
562 machines
.setdefault(_machine
, vm(
563 os
.path
.normpath(self
.path
+ "/" + self
.config
[_machine
]["xml_file"]),
564 os
.path
.normpath(self
.path
+ "/" + self
.config
[_machine
]["snapshot_xml_file"]),
565 self
.config
[_machine
]["image"],
566 self
.config
[_machine
]["root_uid"],
567 self
.config
[_machine
]["username"],
568 self
.config
[_machine
]["password"]))
573 def machine_names(self
):
577 def network_names(self
):
581 if __name__
== "__main__":
584 parser
= argparse
.ArgumentParser()
586 parser
.add_argument("-d", "--directory", dest
="dir")
588 args
= parser
.parse_args()
590 currenttest
= test(args
.dir)
591 currenttest
.read_settings()
592 currenttest
.virtual_environ_setup()
593 currenttest
.load_recipe()
594 currenttest
.virtual_environ_start()
595 currenttest
.run_recipe()
596 currenttest
.virtual_environ_stop()