]>
Commit | Line | Data |
---|---|---|
5e7f6db7 JS |
1 | #!/usr/bin/python3 |
2 | ||
6632e137 | 3 | import configparser |
5e7f6db7 | 4 | import libvirt |
6632e137 | 5 | import logging |
5e7f6db7 | 6 | import os |
6c352a80 JS |
7 | import time |
8 | ||
b560f31a JS |
9 | from . import recipe |
10 | from . import virtual_environ | |
1795f5e8 | 11 | from . import settings |
4ac093dc | 12 | from . import cmd |
5e7f6db7 | 13 | |
1ed8ca9f | 14 | logger = logging.getLogger("nitsi.test") |
5e7f6db7 | 15 | |
61b44c10 JS |
16 | |
17 | class TestException(Exception): | |
18 | def __init__(self, message): | |
19 | self.message = message | |
20 | ||
ee227ea1 | 21 | class Test(): |
ab5b8849 | 22 | def __init__(self, log_path, dir=None, recipe_file=None, settings_file=None, cmd_settings=None): |
978854bf JS |
23 | # init settings var |
24 | self.settings = {} | |
25 | ||
1ec6548b JS |
26 | # Set default values for the settings dict |
27 | self.settings["name"] = "" | |
28 | self.settings["description"] = "" | |
29 | self.settings["copy_from"] = None | |
30 | self.settings["copy_to"] = None | |
31 | self.settings["virtual_environ_path"] = None | |
4ac093dc | 32 | self.settings["interactive_error_handling"] = True |
1ec6548b | 33 | |
ab5b8849 JS |
34 | self.cmd_settings = cmd_settings |
35 | self.log_path = log_path | |
5e7f6db7 | 36 | |
ab5b8849 JS |
37 | # Init all vars with None |
38 | self.settings_file = None | |
39 | self.recipe_file = None | |
40 | self.path = None | |
5e7f6db7 | 41 | |
ab5b8849 JS |
42 | # We need at least a path to a recipe file or a dir to a test |
43 | if not dir and not recipe: | |
44 | raise TestException("Did not get a path to a test or to a recipe file") | |
45 | ||
46 | # We cannot decide which to use when we get both | |
47 | if (dir and recipe_file) or (dir and settings_file): | |
48 | raise TestException("Get dir and path to recipe or settings file") | |
49 | ||
50 | if dir: | |
51 | try: | |
52 | if not os.path.isabs(dir): | |
53 | self.path = os.path.abspath(dir) | |
54 | except BaseException as e: | |
55 | logger.error("Could not get absolute path") | |
56 | raise e | |
57 | ||
58 | logger.debug("Path of this test is: {}".format(self.path)) | |
59 | ||
60 | self.recipe_file = "{}/recipe".format(self.path) | |
61 | self.settings_file = "{}/settings".format(self.path) | |
62 | ||
63 | if recipe_file: | |
64 | if not os.path.isabs(recipe_file): | |
65 | self.recipe_file = os.path.abspath(recipe_file) | |
66 | else: | |
67 | self.recipe_file = recipe_file | |
68 | ||
69 | if settings_file: | |
70 | if not os.path.isabs(settings_file): | |
71 | self.settings_file = os.path.abspath(settings_file) | |
72 | else: | |
73 | self.settings_file = settings_file | |
b4936764 | 74 | |
aadbafc3 JS |
75 | # We can also go on without a settings file |
76 | if self.settings_file: | |
77 | if not os.path.isfile(self.settings_file): | |
78 | logger.error("No such file: {}".format(self.settings_file)) | |
79 | raise TestException("No settings file found") | |
80 | ||
e8da2bf3 JS |
81 | # os.path.isfile fails if self.recipe_file is None so we cannot use an and statement |
82 | if self.recipe_file: | |
83 | if not os.path.isfile(self.recipe_file): | |
aadbafc3 JS |
84 | logger.error("No such file: {}".format(self.recipe_file)) |
85 | raise TestException("No recipe file found") | |
e8da2bf3 JS |
86 | else: |
87 | logger.error("No such file: {}".format(self.recipe_file)) | |
88 | raise TestException("No recipe file found") | |
5e7f6db7 | 89 | |
5e7f6db7 | 90 | |
ab5b8849 JS |
91 | # Init logging |
92 | if dir: | |
93 | self.log = logger.getChild(os.path.basename(self.path)) | |
94 | ||
95 | if recipe: | |
96 | self.log = logger.getChild(os.path.basename(self.recipe_file)) | |
978854bf | 97 | |
5e7f6db7 | 98 | def read_settings(self): |
ab5b8849 JS |
99 | if self.settings_file: |
100 | self.log.debug("Going to read all settings from the ini file") | |
101 | try: | |
102 | self.config = configparser.ConfigParser() | |
103 | self.config.read(self.settings_file) | |
104 | except BaseException as e: | |
105 | self.log.error("Failed to parse the config") | |
106 | raise e | |
107 | ||
108 | self.settings["name"] = self.config.get("GENERAL","name", fallback="") | |
109 | self.settings["description"] = self.config.get("GENERAL", "description", fallback="") | |
110 | self.settings["copy_to"] = self.config.get("GENERAL", "copy_to", fallback=None) | |
111 | self.settings["copy_from"] = self.config.get("GENERAL", "copy_from", fallback=None) | |
112 | self.settings["virtual_environ_path"] = self.config.get("VIRTUAL_ENVIRONMENT", "path", fallback=None) | |
8428a452 | 113 | |
1795f5e8 JS |
114 | # We need to parse some settings here because they are loaded from a settings file |
115 | if not os.path.isabs(self.settings["virtual_environ_path"]): | |
116 | self.settings["virtual_environ_path"] = os.path.normpath(os.path.dirname( | |
117 | self.settings_file) + "/" + self.settings["virtual_environ_path"]) | |
129c0b0f | 118 | |
1795f5e8 JS |
119 | # Parse copy_from setting |
120 | if self.settings["copy_from"]: | |
121 | self.settings["copy_from"] = settings.settings_parse_copy_from(self.settings["copy_from"], | |
122 | path=os.path.dirname(self.settings_file)) | |
14cd493f | 123 | |
1795f5e8 JS |
124 | # Update all settings from the cmd |
125 | self.settings.update(self.cmd_settings) | |
f010720b | 126 | |
1795f5e8 JS |
127 | if not os.path.isabs(self.settings["virtual_environ_path"]): |
128 | self.settings["virtual_environ_path"] = os.path.abspath(self.settings["virtual_environ_path"]) | |
f010720b | 129 | |
14cd493f | 130 | |
1795f5e8 JS |
131 | # Check if we get at least a valid a valid path to virtual environ |
132 | if not self.settings["virtual_environ_path"]: | |
133 | self.log.error("No path for virtual environment found.") | |
134 | raise TestException("No path for virtual environment found.") | |
5e7f6db7 | 135 | |
1795f5e8 JS |
136 | # Print all settings for debugging purpose |
137 | self.log.debug("Settings are:") | |
138 | for key in self.settings: | |
139 | self.log.debug("{}: {}".format(key, self.settings[key])) | |
129c0b0f | 140 | |
5e7f6db7 | 141 | |
3dac9881 | 142 | def virtual_environ_setup_stage_1(self): |
d9f6c37f | 143 | self.virtual_environ = virtual_environ.VirtualEnviron(self.settings["virtual_environ_path"]) |
5e7f6db7 JS |
144 | |
145 | self.virtual_networks = self.virtual_environ.get_networks() | |
146 | ||
147 | self.virtual_machines = self.virtual_environ.get_machines() | |
148 | ||
3dac9881 | 149 | def virtual_environ_setup_stage_2(self): |
fbaed8c3 JS |
150 | # built up which machines which are used in our recipe |
151 | used_machines = [] | |
152 | ||
153 | for line in self.recipe.recipe: | |
154 | if not line[0] in used_machines: | |
155 | used_machines.append(line[0]) | |
156 | ||
157 | self.log.debug("Machines used in this recipe {}".format(used_machines)) | |
158 | ||
159 | self.used_machine_names = used_machines | |
160 | ||
161 | for machine in self.used_machine_names: | |
162 | if not machine in self.virtual_environ.machine_names: | |
163 | raise TestException("{} is listed as machine in the recipe, but the virtual environmet does not have such a machine".format(machine)) | |
164 | ||
165 | ||
5e7f6db7 | 166 | def virtual_environ_start(self): |
3fa89b7c JS |
167 | for name in self.virtual_environ.network_names: |
168 | self.virtual_networks[name].define() | |
169 | self.virtual_networks[name].start() | |
5e7f6db7 | 170 | |
fbaed8c3 | 171 | for name in self.used_machine_names: |
3fa89b7c JS |
172 | self.virtual_machines[name].define() |
173 | self.virtual_machines[name].create_snapshot() | |
8428a452 | 174 | # We can only copy files when we know which and to which dir |
978854bf JS |
175 | if self.settings["copy_from"] and self.settings["copy_to"]: |
176 | self.virtual_machines[name].copy_in(self.settings["copy_from"], self.settings["copy_to"]) | |
3fa89b7c | 177 | self.virtual_machines[name].start() |
5e7f6db7 | 178 | |
6c352a80 JS |
179 | # Time to which all serial output log entries are relativ |
180 | log_start_time = time.time() | |
181 | ||
fc35cba1 JS |
182 | # Number of chars of the longest machine name |
183 | longest_machine_name = self.virtual_environ.longest_machine_name | |
184 | ||
25572214 | 185 | self.log.info("Try to intialize the serial connection, connect and login on all machines") |
fbaed8c3 | 186 | for name in self.used_machine_names: |
25572214 JS |
187 | self.log.info("Try to initialize the serial connection connect and login on {}".format(name)) |
188 | self.virtual_machines[name].serial_init(log_file="{}/test.log".format(self.log_path), | |
fc35cba1 JS |
189 | log_start_time=log_start_time, |
190 | longest_machine_name=longest_machine_name) | |
25572214 | 191 | self.virtual_machines[name].serial_connect() |
5e7f6db7 | 192 | |
3fa89b7c | 193 | def load_recipe(self): |
2fa4467d | 194 | self.log.info("Going to load the recipe") |
3fa89b7c | 195 | try: |
5945ce2a JS |
196 | self.recipe = recipe.Recipe(self.recipe_file, |
197 | fallback_machines=self.virtual_environ.machine_names) | |
198 | ||
4bc54b45 JS |
199 | for line in self.recipe.recipe: |
200 | self.log.debug(line) | |
2fa4467d JS |
201 | |
202 | self.log.debug("This was the recipe") | |
4bc54b45 | 203 | except BaseException as e: |
3fa89b7c | 204 | self.log.error("Failed to load recipe") |
4bc54b45 | 205 | raise e |
3fa89b7c | 206 | |
4ac093dc JS |
207 | # This functions tries to handle an rror of the test (eg. when 'echo "Hello World"' failed) |
208 | # in an interactive way | |
209 | # returns False when the test should exit right now, and True when the test should go on | |
210 | def interactive_error_handling(self): | |
211 | if not self.settings["interactive_error_handling"]: | |
212 | return False | |
213 | ||
214 | _cmd = cmd.CMD(intro="You are droppped into an interative debugging shell because of the previous errors", | |
215 | help={"exit": "Exit the test rigth now", | |
216 | "continue": "Continues the test without any error handling, so do not expect that the test succeeds.", | |
217 | "debug": "Disconnects from the serial console and prints the devices to manually connect to the virtual machines." \ | |
218 | "This is useful when you can fix th error with some manual commands. Please disconnect from the serial consoles and " \ | |
219 | "choose 'exit or 'continue' when you are done"}) | |
220 | ||
221 | command = _cmd.get_input(valid_commands=["continue", "exit", "debug"]) | |
222 | ||
223 | if command == "continue": | |
224 | # The test should go on but we do not any debugging, so we return True | |
225 | return True | |
226 | elif command == "exit": | |
227 | # The test should exit right now (normal behaviour) | |
228 | return False | |
229 | ||
230 | # If we get here we are in debugging mode | |
231 | # Disconnect from the serial console: | |
232 | ||
233 | for name in self.used_machine_names: | |
234 | _cmd.print_to_cmd("Disconnect from the serial console of {}".format(name)) | |
235 | self.virtual_machines[name].serial_disconnect() | |
236 | ||
237 | # Print the serial device for each machine | |
238 | for name in self.used_machine_names: | |
239 | device = self.virtual_machines[name].get_serial_device() | |
240 | _cmd.print_to_cmd("Serial device of {} is {}".format(name, device)) | |
241 | ||
242 | _cmd.print_to_cmd("You can now connect to all serial devices, and send custom commands to the virtual machines." \ | |
243 | "Please type 'continue' or 'exit' when you disconnected from als serial devices and want to go on.") | |
244 | ||
245 | command = _cmd.get_input(valid_commands=["continue", "exit"]) | |
246 | ||
247 | if command == "exit": | |
248 | return False | |
249 | ||
250 | # We should continue whit the test | |
251 | # Reconnect to the serial devices | |
252 | ||
253 | for name in self.used_machine_names: | |
254 | self.log.info("Try to reconnect to {}".format(name)) | |
255 | self.virtual_machines[name].serial_connect() | |
256 | ||
257 | return True | |
258 | ||
3fa89b7c JS |
259 | def run_recipe(self): |
260 | for line in self.recipe.recipe: | |
261 | return_value = self.virtual_machines[line[0]].cmd(line[2]) | |
bce7d520 JS |
262 | self.log.debug("Return value is: {}".format(return_value)) |
263 | if return_value != "0" and line[1] == "": | |
4ac093dc JS |
264 | err_msg = "Failed to execute command '{}' on {}, return code: {}".format(line[2],line[0], return_value) |
265 | # Try to handle this error in an interactive way, if we cannot go on | |
266 | # raise an exception and exit | |
267 | if not self.interactive_error_handling(): | |
268 | raise TestException(err_msg) | |
269 | ||
bce7d520 | 270 | elif return_value == "0" and line[1] == "!": |
4ac093dc JS |
271 | err_msg = "Succeded to execute command '{}' on {}, return code: {}".format(line[2],line[0],return_value) |
272 | self.log.error(err_msg) | |
273 | # Try to handle this error in an interactive way, if we cannot go on | |
274 | # raise an exception and exit | |
275 | if not self.interactive_error_handling(): | |
276 | raise TestException(err_msg) | |
bce7d520 JS |
277 | else: |
278 | self.log.debug("Command '{}' on {} returned with: {}".format(line[2],line[0],return_value)) | |
3fa89b7c JS |
279 | |
280 | def virtual_environ_stop(self): | |
fbaed8c3 | 281 | for name in self.used_machine_names: |
f9178ad3 JS |
282 | # We just catch exception here to avoid |
283 | # that we stop the cleanup process if only one command fails | |
284 | try: | |
285 | self.virtual_machines[name].shutdown() | |
286 | self.virtual_machines[name].revert_snapshot() | |
287 | self.virtual_machines[name].undefine() | |
288 | except BaseException as e: | |
289 | self.log.exception(e) | |
3fa89b7c JS |
290 | |
291 | for name in self.virtual_environ.network_names: | |
f9178ad3 JS |
292 | try: |
293 | self.virtual_networks[name].undefine() | |
294 | except BaseException as e: | |
295 | self.log.exception(e) | |
296 |