From eb8e078a29d00632e4d868b7ce8578fe88ea43b4 Mon Sep 17 00:00:00 2001 From: sBubshait Date: Tue, 5 Aug 2025 15:56:42 +0300 Subject: [PATCH] feat: smarter date choosing and fixing nfs mount path --- activationDrill/drillServer.py | 232 +++++++++++++++++++++------------ 1 file changed, 147 insertions(+), 85 deletions(-) diff --git a/activationDrill/drillServer.py b/activationDrill/drillServer.py index 06074bf..cea3ce2 100644 --- a/activationDrill/drillServer.py +++ b/activationDrill/drillServer.py @@ -63,7 +63,10 @@ class ActivationDrillServer: return [line.strip() for line in output.split('\n') if line.strip()] def get_dataset_snapshots(self, dataset: str) -> List[Dict]: - """Get all snapshots for a dataset with timestamps""" + """Get snapshots for a dataset that match PDG naming convention""" + dataset_basename = dataset.split('/')[-1] + expected_prefix = f"{dataset.replace('/', '-')}-" + success, output = self.run_command(f"zfs list -H -o name,creation -t snapshot | grep '^{dataset}@'") if not success: return [] @@ -76,13 +79,16 @@ class ActivationDrillServer: snap_name = parts[0].strip() creation = parts[1].strip() - # Extract timestamp from snapshot name (assuming default format) + # Extract snapshot suffix (after @) snap_suffix = snap_name.split('@')[1] - snapshots.append({ - 'name': snap_name, - 'suffix': snap_suffix, - 'creation': creation - }) + + # Only include snapshots that match PDG naming convention + if snap_suffix.startswith(expected_prefix): + snapshots.append({ + 'name': snap_name, + 'suffix': snap_suffix, + 'creation': creation + }) # Sort by creation time (newest first) snapshots.sort(key=lambda x: x['creation'], reverse=True) @@ -144,121 +150,175 @@ class ActivationDrillServer: return None def select_snapshot_by_date(self, dataset: str) -> Optional[str]: - """Interactive snapshot selection by date""" + """Hierarchical snapshot selection by year/month/day""" snapshots = self.get_dataset_snapshots(dataset) if not snapshots: - print(f"{Colors.RED}✗ No snapshots found for {dataset}{Colors.ENDC}") + print(f"{Colors.RED}✗ No PDG snapshots found for {dataset}{Colors.ENDC}") return None - # Group snapshots by date - date_groups = {} + # Parse all snapshots and group hierarchically + parsed_snapshots = [] for snap in snapshots: - # Parse snapshot suffix: format is [dataset-name]-YYYY-MM-DD_HH-MM-SS - # Example: pool1-hanaStandby_nfs-2025-08-05_15-36-10 try: suffix = snap['suffix'] - - # Find the last occurrence of a date pattern (YYYY-MM-DD) - # This handles cases where dataset names contain dashes import re - date_pattern = r'(\d{4}-\d{2}-\d{2})_(\d{2}-\d{2}-\d{2})' + date_pattern = r'(\d{4})-(\d{2})-(\d{2})_(\d{2})-(\d{2})-(\d{2})' match = re.search(date_pattern, suffix) if match: - date_part = match.group(1) # YYYY-MM-DD - time_part = match.group(2) # HH-MM-SS - - if date_part not in date_groups: - date_groups[date_part] = [] - - # Store both date and time info - snap['parsed_date'] = date_part - snap['parsed_time'] = time_part.replace('-', ':') - date_groups[date_part].append(snap) - - except Exception as e: - print(f"{Colors.DIM}Warning: Could not parse snapshot {snap['suffix']}: {e}{Colors.ENDC}") + year, month, day, hour, minute, second = match.groups() + parsed_snapshots.append({ + **snap, + 'year': year, + 'month': month, + 'day': day, + 'time': f"{hour}:{minute}:{second}", + 'full_date': f"{year}-{month}-{day}" + }) + except Exception: continue - if not date_groups: - print(f"{Colors.RED}✗ No properly formatted snapshots found{Colors.ENDC}") - print(f"{Colors.DIM}Expected format: [dataset-name]-YYYY-MM-DD_HH-MM-SS{Colors.ENDC}") + if not parsed_snapshots: + print(f"{Colors.RED}✗ No properly formatted PDG snapshots found{Colors.ENDC}") return None - dates = sorted(date_groups.keys(), reverse=True) - earliest_date = min(dates) - latest_date = max(dates) + # Group by year, month, day hierarchically + years = {} + for snap in parsed_snapshots: + year = snap['year'] + month = snap['month'] + day = snap['day'] + + if year not in years: + years[year] = {} + if month not in years[year]: + years[year][month] = {} + if day not in years[year][month]: + years[year][month][day] = [] + + years[year][month][day].append(snap) - print(f"\n{Colors.BOLD}📅 Snapshot Date Selection{Colors.ENDC}") - print(f"{Colors.DIM}({len(snapshots)}) Snapshots available from {earliest_date} to {latest_date}{Colors.ENDC}") + # Hierarchical selection + selected_year = self._select_year(years) if len(years) > 1 else list(years.keys())[0] + if not selected_year: + return None + + selected_month = self._select_month(years[selected_year], selected_year) if len(years[selected_year]) > 1 else list(years[selected_year].keys())[0] + if not selected_month: + return None + + selected_day = self._select_day(years[selected_year][selected_month], selected_year, selected_month) if len(years[selected_year][selected_month]) > 1 else list(years[selected_year][selected_month].keys())[0] + if not selected_day: + return None - # Show available dates - print(f"\n{Colors.BOLD}Available Dates:{Colors.ENDC}") - for i, date in enumerate(dates, 1): - count = len(date_groups[date]) - # Format date nicely - try: - from datetime import datetime - date_obj = datetime.strptime(date, '%Y-%m-%d') - formatted_date = date_obj.strftime('%B %d, %Y (%A)') - print(f"{Colors.CYAN}{i:2d}.{Colors.ENDC} {formatted_date} ({count} snapshot{'s' if count > 1 else ''})") - except: - print(f"{Colors.CYAN}{i:2d}.{Colors.ENDC} {date} ({count} snapshot{'s' if count > 1 else ''})") + # Select specific snapshot from the chosen day + day_snapshots = years[selected_year][selected_month][selected_day] + return self._select_time(day_snapshots, selected_year, selected_month, selected_day) + + def _select_year(self, years: Dict) -> Optional[str]: + """Select year from available years""" + year_list = sorted(years.keys(), reverse=True) + + print(f"\n{Colors.BOLD}📅 Year Selection{Colors.ENDC}") + for i, year in enumerate(year_list, 1): + total_snapshots = sum(len(day_snaps) for month in years[year].values() for day_snaps in month.values()) + print(f"{Colors.CYAN}{i:2d}.{Colors.ENDC} {year} ({total_snapshots} snapshots)") - # Select date while True: try: - choice = input(f"\n{Colors.BOLD}Select date (1-{len(dates)}): {Colors.ENDC}") + choice = input(f"\n{Colors.BOLD}Select year (1-{len(year_list)}): {Colors.ENDC}") idx = int(choice) - 1 - if 0 <= idx < len(dates): - selected_date = dates[idx] - break + if 0 <= idx < len(year_list): + return year_list[idx] print(f"{Colors.RED}Invalid selection{Colors.ENDC}") except (ValueError, KeyboardInterrupt): return None + + def _select_month(self, months: Dict, year: str) -> Optional[str]: + """Select month from available months""" + month_list = sorted(months.keys(), reverse=True) + month_names = ["", "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December"] - # Show snapshots for selected date - day_snapshots = date_groups[selected_date] + print(f"\n{Colors.BOLD}📅 Month Selection for {year}{Colors.ENDC}") + for i, month in enumerate(month_list, 1): + total_snapshots = sum(len(day_snaps) for day_snaps in months[month].values()) + month_name = month_names[int(month)] if 1 <= int(month) <= 12 else month + print(f"{Colors.CYAN}{i:2d}.{Colors.ENDC} {month_name} ({total_snapshots} snapshots)") - # Format the selected date nicely - try: - from datetime import datetime - date_obj = datetime.strptime(selected_date, '%Y-%m-%d') - formatted_date = date_obj.strftime('%B %d, %Y') - except: - formatted_date = selected_date - - print(f"\n{Colors.BOLD}Snapshots for {formatted_date}:{Colors.ENDC}") + while True: + try: + choice = input(f"\n{Colors.BOLD}Select month (1-{len(month_list)}): {Colors.ENDC}") + idx = int(choice) - 1 + if 0 <= idx < len(month_list): + return month_list[idx] + print(f"{Colors.RED}Invalid selection{Colors.ENDC}") + except (ValueError, KeyboardInterrupt): + return None + + def _select_day(self, days: Dict, year: str, month: str) -> Optional[str]: + """Select day from available days""" + day_list = sorted(days.keys(), reverse=True) + month_names = ["", "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December"] + month_name = month_names[int(month)] if 1 <= int(month) <= 12 else month - for i, snap in enumerate(day_snapshots, 1): - time_display = snap.get('parsed_time', 'Unknown time') - # Make creation time more readable + print(f"\n{Colors.BOLD}📅 Day Selection for {month_name} {year}{Colors.ENDC}") + for i, day in enumerate(day_list, 1): + snapshot_count = len(days[day]) + try: + from datetime import datetime + date_obj = datetime(int(year), int(month), int(day)) + day_name = date_obj.strftime('%A') + print(f"{Colors.CYAN}{i:2d}.{Colors.ENDC} {day_name}, {month_name} {day} ({snapshot_count} snapshots)") + except: + print(f"{Colors.CYAN}{i:2d}.{Colors.ENDC} Day {day} ({snapshot_count} snapshots)") + + while True: + try: + choice = input(f"\n{Colors.BOLD}Select day (1-{len(day_list)}): {Colors.ENDC}") + idx = int(choice) - 1 + if 0 <= idx < len(day_list): + return day_list[idx] + print(f"{Colors.RED}Invalid selection{Colors.ENDC}") + except (ValueError, KeyboardInterrupt): + return None + + def _select_time(self, snapshots: List[Dict], year: str, month: str, day: str) -> Optional[str]: + """Select specific snapshot by time""" + month_names = ["", "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December"] + month_name = month_names[int(month)] if 1 <= int(month) <= 12 else month + + # Sort by time (newest first) + snapshots.sort(key=lambda x: x['time'], reverse=True) + + print(f"\n{Colors.BOLD}🕐 Time Selection for {month_name} {day}, {year}{Colors.ENDC}") + + for i, snap in enumerate(snapshots, 1): creation_parts = snap['creation'].split() if len(creation_parts) >= 4: - # Show just date and time, skip timezone and year if same creation_display = f"{creation_parts[1]} {creation_parts[2]} {creation_parts[3]}" else: creation_display = snap['creation'] - print(f"{Colors.CYAN}{i:2d}.{Colors.ENDC} {time_display} {Colors.DIM}(created: {creation_display}){Colors.ENDC}") + print(f"{Colors.CYAN}{i:2d}.{Colors.ENDC} {snap['time']} {Colors.DIM}(created: {creation_display}){Colors.ENDC}") - # Select specific snapshot while True: try: - choice = input(f"\n{Colors.BOLD}Select snapshot (1-{len(day_snapshots)}): {Colors.ENDC}") + choice = input(f"\n{Colors.BOLD}Select snapshot (1-{len(snapshots)}): {Colors.ENDC}") idx = int(choice) - 1 - if 0 <= idx < len(day_snapshots): - selected_snap = day_snapshots[idx]['name'] - selected_time = day_snapshots[idx].get('parsed_time', 'Unknown') + if 0 <= idx < len(snapshots): + selected_snap = snapshots[idx] # Confirm selection - print(f"\n{Colors.YELLOW}Selected: {formatted_date} at {selected_time}{Colors.ENDC}") - print(f"{Colors.DIM}Snapshot: {selected_snap}{Colors.ENDC}") + print(f"\n{Colors.YELLOW}Selected: {month_name} {day}, {year} at {selected_snap['time']}{Colors.ENDC}") + print(f"{Colors.DIM}Snapshot: {selected_snap['name']}{Colors.ENDC}") confirm = input(f"{Colors.BOLD}Proceed with this snapshot? (y/N): {Colors.ENDC}") if confirm.lower() == 'y': - return selected_snap + return selected_snap['name'] else: - return self.select_snapshot_by_date(dataset) # Start over + return self.select_snapshot_by_date(selected_snap['name'].split('@')[0]) # Start over print(f"{Colors.RED}Invalid selection{Colors.ENDC}") except (ValueError, KeyboardInterrupt): return None @@ -285,16 +345,18 @@ class ActivationDrillServer: def update_nfs_exports(self, clone_name: str) -> Tuple[bool, str]: """Add clone to NFS exports if not already present""" + # Use full pool path for NFS export + export_path = f"/{clone_name}" + # Check if clone is already in exports if os.path.exists(self.nfs_exports_file): with open(self.nfs_exports_file, 'r') as f: content = f.read() - if clone_name in content: + if export_path in content: return True, "NFS export already exists" - # Add to exports - mount_point = f"/{clone_name.split('/')[-1]}" - export_line = f"{mount_point} *(rw,sync,no_root_squash,no_subtree_check)\n" + # Add to exports with full path + export_line = f"{export_path} *(rw,sync,no_root_squash,no_subtree_check)\n" try: with open(self.nfs_exports_file, 'a') as f: @@ -303,7 +365,7 @@ class ActivationDrillServer: # Reload NFS exports success, output = self.run_command("exportfs -ra") if success: - return True, f"NFS export added: {mount_point}" + return True, f"NFS export added: {export_path}" else: return False, f"Failed to reload NFS exports: {output}" except Exception as e: