+1
capsulflask/db_model.py
+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
+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
+9
capsulflask/shell_scripts/is-backup-currently-running.sh
+3
-2
capsulflask/spoke_api.py
+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
+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),