capsul.org webapp

detect if backup is already running

forest 1cb757ff 56043cbd

+1
capsulflask/db_model.py
··· 227 227 228 228 229 229 def get_next_vm_to_back_up(self): 230 + # TODO:: to support multiple hosts properly, this should probably group by host and return multiple rows for multiple hosts. 230 231 self.cursor.execute(""" 231 232 SELECT id, email, host, last_backup_started, last_backup_finished, backup_window_start_hour_utc FROM vms 232 233 WHERE deleted is null AND has_guest_agent = true AND (
+5 -1
capsulflask/hub_model.py
··· 270 270 except: 271 271 pass 272 272 273 + if result_status == "busy": 274 + current_app.logger.info(f"failed to start backup on '{id}' on host '{host}' because backup is already running") 275 + return 276 + 273 277 if not result_status == "success": 274 - raise ValueError(f"""failed to start backup on "{id}" on host "{host}" for {email}: {result_json_string}""") 278 + raise ValueError(f"failed to start backup on '{id}' on host '{host}' for {email}: {result_json_string}") 275 279 276 280 def destroy(self, email: str, id: str): 277 281 validate_capsul_id(id)
+9
capsulflask/shell_scripts/is-backup-currently-running.sh
··· 1 + #!/bin/sh -e 2 + 3 + if ps aux | grep -q '[v]irtnbdbackup'; then 4 + echo "yes" 5 + else 6 + echo "no" 7 + fi 8 + 9 +
+3 -2
capsulflask/spoke_api.py
··· 268 268 current_app.logger.error(f"/hosts/operation returned 400: {required_property} is required for handle_start_backup") 269 269 return abort(400, f"bad request; {required_property} is required for handle_start_backup") 270 270 271 + return_status = "error" 271 272 try: 272 - current_app.config['SPOKE_MODEL'].start_backup(email=request_body['email'], id=request_body['id']) 273 + return_status = current_app.config['SPOKE_MODEL'].start_backup(email=request_body['email'], id=request_body['id']) 273 274 except: 274 275 error_message = my_exec_info_message(sys.exc_info()) 275 276 params= f"email='{request_body['email'] if 'email' in request_body else 'KeyError'}', " ··· 277 278 current_app.logger.error(f"current_app.config['SPOKE_MODEL'].start_backup({params}) failed: {error_message}") 278 279 return jsonify(dict(assignment_status="assigned", status="error", error_message=error_message)) 279 280 280 - return jsonify(dict(assignment_status="assigned", status="success")) 281 + return jsonify(dict(assignment_status="assigned", status=return_status))
+46 -38
capsulflask/spoke_model.py
··· 78 78 def __init__(self, flask_app): 79 79 self.flask_app = flask_app 80 80 81 - def validate_completed_process(self, completedProcess, email=None): 81 + def validate_completed_process(self, completed_process, email=None): 82 82 emailPart = "" 83 83 if email != None: 84 84 emailPart = f"for {email}" 85 85 86 - if completedProcess.returncode != 0: 87 - raise RuntimeError(f"""{" ".join(completedProcess.args)} failed {emailPart} with exit code {completedProcess.returncode} 86 + if completed_process.returncode != 0: 87 + raise RuntimeError(f"""{" ".join(completed_process.args)} failed {emailPart} with exit code {completed_process.returncode} 88 88 stdout: 89 - {completedProcess.stdout} 89 + {completed_process.stdout} 90 90 stderr: 91 - {completedProcess.stderr} 91 + {completed_process.stderr} 92 92 """) 93 93 94 94 def capacity_avaliable(self, additional_ram_bytes): 95 95 my_args=[join(current_app.root_path, 'shell_scripts/capacity-avaliable.sh'), str(additional_ram_bytes)] 96 - completedProcess = run(my_args, capture_output=True) 96 + completed_process = run(my_args, capture_output=True) 97 97 98 - if completedProcess.returncode != 0: 98 + if completed_process.returncode != 0: 99 99 current_app.logger.error(f""" 100 - capacity-avaliable.sh exited {completedProcess.returncode} with 100 + capacity-avaliable.sh exited {completed_process.returncode} with 101 101 stdout: 102 - {completedProcess.stdout} 102 + {completed_process.stdout} 103 103 stderr: 104 - {completedProcess.stderr} 104 + {completed_process.stderr} 105 105 """) 106 106 return False 107 107 108 - lines = completedProcess.stdout.splitlines() 108 + lines = completed_process.stdout.splitlines() 109 109 output = lines[len(lines)-1] 110 110 if not output == b"yes": 111 111 current_app.logger.error(f"capacity-avaliable.sh exited 0 and returned {output} but did not return \"yes\" ") ··· 115 115 116 116 def get(self, id, get_ssh_host_keys): 117 117 validate_capsul_id(id) 118 - completedProcess = run([join(current_app.root_path, 'shell_scripts/get.sh'), id], capture_output=True) 119 - self.validate_completed_process(completedProcess) 120 - lines = completedProcess.stdout.splitlines() 118 + completed_process = run([join(current_app.root_path, 'shell_scripts/get.sh'), id], capture_output=True) 119 + self.validate_completed_process(completed_process) 120 + lines = completed_process.stdout.splitlines() 121 121 if len(lines) == 0: 122 122 current_app.logger.warning("shell_scripts/get.sh returned zero lines!") 123 123 return None ··· 144 144 145 145 if get_ssh_host_keys: 146 146 try: 147 - completedProcess2 = run([join(current_app.root_path, 'shell_scripts/ssh-keyscan.sh'), ipaddr], capture_output=True) 148 - self.validate_completed_process(completedProcess2) 149 - ssh_host_keys = json.loads(completedProcess2.stdout.decode("utf-8")) 147 + completed_process2 = run([join(current_app.root_path, 'shell_scripts/ssh-keyscan.sh'), ipaddr], capture_output=True) 148 + self.validate_completed_process(completed_process2) 149 + ssh_host_keys = json.loads(completed_process2.stdout.decode("utf-8")) 150 150 return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"], state=state, ipv4=ipaddr, ssh_host_keys=ssh_host_keys) 151 151 except: 152 152 current_app.logger.warning(f""" ··· 249 249 250 250 ssh_keys_string = "\n".join(ssh_authorized_keys) 251 251 252 - completedProcess = run([ 252 + completed_process = run([ 253 253 join(current_app.root_path, 'shell_scripts/create.sh'), 254 254 id, 255 255 template_image_file_name, ··· 260 260 public_ipv4 261 261 ], capture_output=True) 262 262 263 - self.validate_completed_process(completedProcess, email) 264 - lines = completedProcess.stdout.splitlines() 263 + self.validate_completed_process(completed_process, email) 264 + lines = completed_process.stdout.splitlines() 265 265 status = lines[len(lines)-1].decode("utf-8") 266 266 267 267 vmSettings = f""" ··· 278 278 raise ValueError(f"""failed to create vm for {email} with: 279 279 {vmSettings} 280 280 stdout: 281 - {completedProcess.stdout} 281 + {completed_process.stdout} 282 282 stderr: 283 - {completedProcess.stderr} 283 + {completed_process.stderr} 284 284 """) 285 285 286 286 def destroy(self, email: str, id: str): 287 287 validate_capsul_id(id) 288 - completedProcess = run([join(current_app.root_path, 'shell_scripts/destroy.sh'), id], capture_output=True) 289 - self.validate_completed_process(completedProcess, email) 290 - lines = completedProcess.stdout.splitlines() 288 + completed_process = run([join(current_app.root_path, 'shell_scripts/destroy.sh'), id], capture_output=True) 289 + self.validate_completed_process(completed_process, email) 290 + lines = completed_process.stdout.splitlines() 291 291 status = lines[len(lines)-1].decode("utf-8") 292 292 if not status == "success": 293 293 raise ValueError(f"""failed to destroy vm {id} for {email} on {current_app.config["SPOKE_HOST_ID"]}: 294 294 stdout: 295 - {completedProcess.stdout} 295 + {completed_process.stdout} 296 296 stderr: 297 - {completedProcess.stderr} 297 + {completed_process.stderr} 298 298 """) 299 299 300 300 def vm_state_command(self, email: str, id: str, command: str): ··· 302 302 if command not in ["stop", "force-stop", "start", "restart"]: 303 303 raise ValueError(f"command ({command}) must be one of stop, force-stop, start, or restart") 304 304 305 - completedProcess = run([join(current_app.root_path, f"shell_scripts/{command}.sh"), id], capture_output=True) 306 - self.validate_completed_process(completedProcess, email) 307 - returned_string = completedProcess.stdout.decode("utf-8") 305 + completed_process = run([join(current_app.root_path, f"shell_scripts/{command}.sh"), id], capture_output=True) 306 + self.validate_completed_process(completed_process, email) 307 + returned_string = completed_process.stdout.decode("utf-8") 308 308 current_app.logger.info(f"{command} vm {id} for {email} returned: {returned_string}") 309 309 310 310 def net_set_dhcp(self, email: str, host_id: str, network_name: str, macs: list, remove_ipv4: str, add_ipv4: str): ··· 324 324 raise ValueError(f"remove_ipv4 \"{remove_ipv4}\" must match \"^[0-9.]+$\"") 325 325 326 326 for mac in macs: 327 - completedProcess = run([join(current_app.root_path, f"shell_scripts/ip-dhcp-host.sh"), "delete", network_name, mac, remove_ipv4], capture_output=True) 328 - self.validate_completed_process(completedProcess, email) 327 + completed_process = run([join(current_app.root_path, f"shell_scripts/ip-dhcp-host.sh"), "delete", network_name, mac, remove_ipv4], capture_output=True) 328 + self.validate_completed_process(completed_process, email) 329 329 330 330 if add_ipv4 != None and add_ipv4 != "": 331 331 if not re.match(r"^[0-9.]+$", add_ipv4): 332 332 raise ValueError(f"add_ipv4 \"{add_ipv4}\" must match \"^[0-9.]+$\"") 333 333 334 334 for mac in macs: 335 - completedProcess = run([join(current_app.root_path, f"shell_scripts/ip-dhcp-host.sh"), "add", network_name, mac, add_ipv4], capture_output=True) 336 - self.validate_completed_process(completedProcess, email) 335 + completed_process = run([join(current_app.root_path, f"shell_scripts/ip-dhcp-host.sh"), "add", network_name, mac, add_ipv4], capture_output=True) 336 + self.validate_completed_process(completed_process, email) 337 337 338 - def start_backup(self, email: str, id: str): 338 + def start_backup(self, email: str, id: str) -> str: 339 339 validate_capsul_id(id) 340 340 current_app.logger.info(f"start_backup: {id} for {email}") 341 + 342 + completed_process = run([join(current_app.root_path, f"shell_scripts/is-backup-currently-running.sh")], capture_output=True) 343 + self.validate_completed_process(completed_process, email) 344 + 345 + if "yes" in completed_process.stdout.decode("utf-8"): 346 + current_app.logger.info(f"start_backup: virtnbdbackup process is already running, exiting...") 347 + return "busy" 341 348 342 349 vm_backup_dirs = [] 343 350 backup_storage_mounts = list(map(lambda x: x.rstrip("/"), current_app.config['BACKUP_STORAGE_MOUNTS'].split(","))) ··· 398 405 current_app.logger.info(f"creating new backup folder w/ new full backup: {new_backup_dir_path}") 399 406 new_thread = threading.Thread(target=self.do_backup, args=(id, "full", new_backup_dir_path)) 400 407 new_thread.start() 401 - current_app.logger.info(f"new_thread.start() finished") 402 408 else: 403 409 current_app.logger.info(f"performing incremental backup to: {most_recent_backup_dir_string}") 404 410 new_thread = threading.Thread(target=self.do_backup, args=(id, "inc", most_recent_backup_dir_string)) 405 411 new_thread.start() 406 - current_app.logger.info(f"new_thread.start() finished") 412 + 413 + current_app.logger.info(f"finished spawning background thread for backup") 414 + return "success" 407 415 408 416 def do_backup(self, id, backup_level, backup_dir_path): 409 417 with self.flask_app.app_context(): ··· 424 432 'Authorization': f"Bearer {current_app.config['SPOKE_HOST_TOKEN']}", 425 433 'Content-Type': "application/json" 426 434 } 427 - notify_backup_finished_url = f"{app.config['HUB_URL']}/hub/backup-finished/{current_app.config['SPOKE_HOST_ID']}" 435 + notify_backup_finished_url = f"{current_app.config['HUB_URL']}/hub/backup-finished/{current_app.config['SPOKE_HOST_ID']}" 428 436 payload = json.dumps({ 429 437 'id': id, 430 438 'command': ' '.join(completed_process.args),