feat: allow managing snapshot automations

This commit is contained in:
sBubshait 2025-08-05 14:27:08 +03:00
parent de64c6d4d5
commit e34f7ada9a

View File

@ -75,6 +75,13 @@ class PDGSnapshot:
if success: if success:
for line in output.split('\n'): for line in output.split('\n'):
if self.cron_prefix in line and '#' in line: if self.cron_prefix in line and '#' in line:
# Check if line is paused (commented out)
is_paused = line.strip().startswith('#') and not line.strip().startswith('##')
if is_paused:
# Remove leading # to parse
line = line.lstrip('#')
parts = line.split('#', 1) parts = line.split('#', 1)
if len(parts) == 2: if len(parts) == 2:
cron_schedule = parts[0].strip() cron_schedule = parts[0].strip()
@ -83,7 +90,7 @@ class PDGSnapshot:
dataset = comment.replace(self.cron_prefix, '') dataset = comment.replace(self.cron_prefix, '')
automations[dataset] = { automations[dataset] = {
'schedule': cron_schedule, 'schedule': cron_schedule,
'status': 'healthy' 'status': 'paused' if is_paused else 'active'
} }
return automations return automations
@ -100,6 +107,9 @@ class PDGSnapshot:
if dataset not in automations: if dataset not in automations:
return "error", "Cron job not found" return "error", "Cron job not found"
if automations[dataset]['status'] == 'paused':
return "paused", "Automation is paused"
return "healthy", "Running normally" return "healthy", "Running normally"
def show_loading(self, message: str, duration: float = 2.0): def show_loading(self, message: str, duration: float = 2.0):
@ -152,6 +162,99 @@ class PDGSnapshot:
except Exception as e: except Exception as e:
return False, f"Failed to update crontab: {str(e)}" return False, f"Failed to update crontab: {str(e)}"
def pause_cron_job(self, dataset: str) -> Tuple[bool, str]:
"""Pause cron job by commenting it out"""
success, current_cron = self.run_command("crontab -l 2>/dev/null")
if not success:
return False, "No crontab found"
lines = []
job_found = False
for line in current_cron.split('\n'):
if line.strip() and self.cron_prefix in line and dataset in line:
if not line.startswith('#'):
lines.append(f"#{line}") # Comment out the line
job_found = True
else:
lines.append(line) # Already paused
job_found = True
else:
lines.append(line)
if not job_found:
return False, "Automation not found"
new_cron = '\n'.join(lines) + '\n'
try:
process = subprocess.Popen(['crontab', '-'], stdin=subprocess.PIPE, text=True)
process.communicate(input=new_cron)
return process.returncode == 0, "Automation paused successfully"
except Exception as e:
return False, f"Failed to pause automation: {str(e)}"
def resume_cron_job(self, dataset: str) -> Tuple[bool, str]:
"""Resume paused cron job by uncommenting it"""
success, current_cron = self.run_command("crontab -l 2>/dev/null")
if not success:
return False, "No crontab found"
lines = []
job_found = False
for line in current_cron.split('\n'):
if line.strip() and self.cron_prefix in line and dataset in line:
if line.startswith('#') and not line.startswith('##'):
lines.append(line[1:]) # Remove comment
job_found = True
else:
lines.append(line) # Already active
job_found = True
else:
lines.append(line)
if not job_found:
return False, "Automation not found"
new_cron = '\n'.join(lines) + '\n'
try:
process = subprocess.Popen(['crontab', '-'], stdin=subprocess.PIPE, text=True)
process.communicate(input=new_cron)
return process.returncode == 0, "Automation resumed successfully"
except Exception as e:
return False, f"Failed to resume automation: {str(e)}"
def delete_cron_job(self, dataset: str) -> Tuple[bool, str]:
"""Delete cron job for dataset"""
success, current_cron = self.run_command("crontab -l 2>/dev/null")
if not success:
return False, "No crontab found"
lines = []
job_found = False
for line in current_cron.split('\n'):
if line.strip() and self.cron_prefix in line and dataset in line:
job_found = True
# Skip this line (delete it)
continue
else:
lines.append(line)
if not job_found:
return False, "Automation not found"
new_cron = '\n'.join(lines) + '\n'
try:
process = subprocess.Popen(['crontab', '-'], stdin=subprocess.PIPE, text=True)
process.communicate(input=new_cron)
return process.returncode == 0, "Automation deleted successfully"
except Exception as e:
return False, f"Failed to delete automation: {str(e)}"
def select_dataset(self) -> Optional[str]: def select_dataset(self) -> Optional[str]:
"""Interactive dataset selection""" """Interactive dataset selection"""
datasets = self.get_zfs_datasets() datasets = self.get_zfs_datasets()
@ -236,6 +339,119 @@ class PDGSnapshot:
return default_format return default_format
def manage_automations(self):
"""Manage existing automations (pause/resume/delete)"""
automations = self.get_existing_automations()
if not automations:
print(f"\n{Colors.YELLOW}No automations found to manage{Colors.ENDC}")
return
print(f"\n{Colors.BOLD}🔧 Manage Automations{Colors.ENDC}")
datasets = list(automations.keys())
# Show automations with status
for i, dataset in enumerate(datasets, 1):
status, _ = self.check_automation_health(dataset)
if status == "healthy":
status_icon = f"{Colors.GREEN}{Colors.ENDC}"
elif status == "paused":
status_icon = f"{Colors.YELLOW}{Colors.ENDC}"
else:
status_icon = f"{Colors.RED}{Colors.ENDC}"
print(f"{Colors.CYAN}{i:2d}.{Colors.ENDC} {status_icon} {dataset}")
# Select automation to manage
while True:
try:
choice = input(f"\n{Colors.BOLD}Select automation (1-{len(datasets)}): {Colors.ENDC}")
idx = int(choice) - 1
if 0 <= idx < len(datasets):
selected_dataset = datasets[idx]
break
print(f"{Colors.RED}Invalid selection{Colors.ENDC}")
except (ValueError, KeyboardInterrupt):
return
# Show management options
status, _ = self.check_automation_health(selected_dataset)
print(f"\n{Colors.BOLD}Manage '{selected_dataset}':{Colors.ENDC}")
options = []
if status == "healthy":
options.append(("1", "Pause automation", "pause"))
elif status == "paused":
options.append(("1", "Resume automation", "resume"))
options.append(("2", f"{Colors.RED}Delete automation{Colors.ENDC}", "delete"))
options.append(("3", "Cancel", "cancel"))
for num, desc, _ in options:
print(f"{Colors.CYAN}{num}.{Colors.ENDC} {desc}")
# Handle action
while True:
action_choice = input(f"\n{Colors.BOLD}Select action: {Colors.ENDC}")
action_map = {opt[0]: opt[2] for opt in options}
if action_choice in action_map:
action = action_map[action_choice]
if action == "cancel":
return
elif action == "pause":
self.show_loading("Pausing automation")
success, message = self.pause_cron_job(selected_dataset)
elif action == "resume":
self.show_loading("Resuming automation")
success, message = self.resume_cron_job(selected_dataset)
elif action == "delete":
confirm = input(f"{Colors.RED}Are you sure? This cannot be undone (y/N): {Colors.ENDC}")
if confirm.lower() != 'y':
return
self.show_loading("Deleting automation")
success, message = self.delete_cron_job(selected_dataset)
if success:
print(f"{Colors.GREEN}{message}{Colors.ENDC}")
else:
print(f"{Colors.RED}{message}{Colors.ENDC}")
return
print(f"{Colors.RED}Invalid selection{Colors.ENDC}")
def show_status(self):
"""Display status of all automations"""
automations = self.get_existing_automations()
if not automations:
print(f"\n{Colors.YELLOW}No automations found{Colors.ENDC}")
return
print(f"\n{Colors.BOLD}📊 Automation Status{Colors.ENDC}")
for dataset, info in automations.items():
status, message = self.check_automation_health(dataset)
if status == "healthy":
icon = f"{Colors.GREEN}{Colors.ENDC}"
status_text = f"{Colors.GREEN}Active{Colors.ENDC}"
elif status == "paused":
icon = f"{Colors.YELLOW}{Colors.ENDC}"
status_text = f"{Colors.YELLOW}Paused{Colors.ENDC}"
else:
icon = f"{Colors.RED}{Colors.ENDC}"
status_text = f"{Colors.RED}Error{Colors.ENDC}"
print(f"\n{icon} {Colors.BOLD}{dataset}{Colors.ENDC}")
print(f" Status: {status_text}")
print(f" Schedule: {Colors.DIM}{info['schedule']}{Colors.ENDC}")
if status == "error":
print(f" Error: {Colors.RED}{message}{Colors.ENDC}")
def create_automation(self): def create_automation(self):
"""Create new snapshot automation""" """Create new snapshot automation"""
print(f"\n{Colors.BOLD}📸 Create New Automation{Colors.ENDC}") print(f"\n{Colors.BOLD}📸 Create New Automation{Colors.ENDC}")
@ -270,33 +486,6 @@ class PDGSnapshot:
print(f"{Colors.RED}✗ Failed to create automation{Colors.ENDC}") print(f"{Colors.RED}✗ Failed to create automation{Colors.ENDC}")
print(f"{Colors.RED}{message}{Colors.ENDC}") print(f"{Colors.RED}{message}{Colors.ENDC}")
def show_status(self):
"""Display status of all automations"""
automations = self.get_existing_automations()
if not automations:
print(f"\n{Colors.YELLOW}No automations found{Colors.ENDC}")
return
print(f"\n{Colors.BOLD}📊 Automation Status{Colors.ENDC}")
for dataset, info in automations.items():
status, message = self.check_automation_health(dataset)
if status == "healthy":
icon = f"{Colors.GREEN}{Colors.ENDC}"
status_text = f"{Colors.GREEN}Healthy{Colors.ENDC}"
else:
icon = f"{Colors.RED}{Colors.ENDC}"
status_text = f"{Colors.RED}Error{Colors.ENDC}"
print(f"\n{icon} {Colors.BOLD}{dataset}{Colors.ENDC}")
print(f" Status: {status_text}")
print(f" Schedule: {Colors.DIM}{info['schedule']}{Colors.ENDC}")
if status == "error":
print(f" Error: {Colors.RED}{message}{Colors.ENDC}")
def main_menu(self): def main_menu(self):
"""Display main menu and handle user input""" """Display main menu and handle user input"""
automations = self.get_existing_automations() automations = self.get_existing_automations()
@ -305,15 +494,26 @@ class PDGSnapshot:
if automation_count > 0: if automation_count > 0:
healthy_count = sum(1 for dataset in automations healthy_count = sum(1 for dataset in automations
if self.check_automation_health(dataset)[0] == "healthy") if self.check_automation_health(dataset)[0] == "healthy")
print(f"{Colors.GREEN}📈 {healthy_count}/{automation_count} Automations Healthy{Colors.ENDC}") paused_count = sum(1 for dataset in automations
if self.check_automation_health(dataset)[0] == "paused")
status_parts = []
if healthy_count > 0:
status_parts.append(f"{Colors.GREEN}{healthy_count} Active{Colors.ENDC}")
if paused_count > 0:
status_parts.append(f"{Colors.YELLOW}{paused_count} Paused{Colors.ENDC}")
status_text = " | ".join(status_parts) if status_parts else f"{Colors.RED}0 Running{Colors.ENDC}"
print(f"{Colors.BOLD}📈 Automations: {status_text}{Colors.ENDC}")
print(f"\n{Colors.BOLD}Options:{Colors.ENDC}") print(f"\n{Colors.BOLD}Options:{Colors.ENDC}")
print(f"{Colors.CYAN}1.{Colors.ENDC} View Status") print(f"{Colors.CYAN}1.{Colors.ENDC} View Status")
print(f"{Colors.CYAN}2.{Colors.ENDC} Create New Automation") print(f"{Colors.CYAN}2.{Colors.ENDC} Create New Automation")
print(f"{Colors.CYAN}3.{Colors.ENDC} Exit") print(f"{Colors.CYAN}3.{Colors.ENDC} Manage Automations")
print(f"{Colors.CYAN}4.{Colors.ENDC} Exit")
while True: while True:
choice = input(f"\n{Colors.BOLD}Select option (1-3): {Colors.ENDC}") choice = input(f"\n{Colors.BOLD}Select option (1-4): {Colors.ENDC}")
if choice == "1": if choice == "1":
self.show_status() self.show_status()
@ -322,6 +522,9 @@ class PDGSnapshot:
self.create_automation() self.create_automation()
break break
elif choice == "3": elif choice == "3":
self.manage_automations()
break
elif choice == "4":
print(f"\n{Colors.DIM}Goodbye!{Colors.ENDC}") print(f"\n{Colors.DIM}Goodbye!{Colors.ENDC}")
sys.exit(0) sys.exit(0)
else: else: