Skip to content

Commit 4288028

Browse files
committed
v3.7: SSL verification toggle with warning, advanced settings UI (timeout/max_backups), granular error reporting, Ruff CI lint step
1 parent 7aada97 commit 4288028

2 files changed

Lines changed: 92 additions & 8 deletions

File tree

.github/workflows/build.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,14 @@ jobs:
2424
run: |
2525
python -m pip install --upgrade pip
2626
pip install -r requirements.txt
27-
27+
pip install ruff
28+
29+
- name: Lint with Ruff
30+
continue-on-error: true
31+
run: |
32+
ruff check AOS-CX.Config.Backup.Tool_3.6.py --select E,F --ignore E501
33+
echo "Lint passed (basic checks)"
34+
2835
- name: Build Windows executable
2936
run: |
3037
pyinstaller .build/AOS-CX.Config.Backup.Tool_Windows.spec --clean --noconfirm

AOS-CX.Config.Backup.Tool_3.6.py

Lines changed: 84 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def __init__(self):
5151
self.wasabi_region = "us-east-1"
5252
self.wasabi_enabled = False
5353
self.last_wasabi_status = "Not attempted"
54+
self.verify_ssl = False
5455
self.root = None
5556
self.systray = None
5657
self.backup_lock = threading.Lock()
@@ -162,7 +163,9 @@ def load_config(self):
162163
'wasabi_secret_key': '',
163164
'wasabi_bucket': '',
164165
'wasabi_region': 'us-east-1',
165-
'wasabi_enabled': False
166+
'wasabi_enabled': False,
167+
'verify_ssl': False,
168+
'max_backups': 5
166169
}
167170
try:
168171
with open(self.config_file, 'r') as f:
@@ -185,7 +188,9 @@ def load_config(self):
185188
self.wasabi_bucket = config.get('wasabi_bucket', default_config['wasabi_bucket'])
186189
self.wasabi_region = config.get('wasabi_region', default_config['wasabi_region'])
187190
self.wasabi_enabled = config.get('wasabi_enabled', default_config['wasabi_enabled'])
188-
logging.info(f"Loaded config: git_enabled={self.git_enabled}, wasabi_enabled={self.wasabi_enabled}")
191+
self.verify_ssl = config.get('verify_ssl', default_config['verify_ssl'])
192+
self.max_backups = config.get('max_backups', default_config['max_backups'])
193+
logging.info(f"Loaded config: git_enabled={self.git_enabled}, wasabi_enabled={self.wasabi_enabled}, verify_ssl={self.verify_ssl}")
189194
except (FileNotFoundError, json.JSONDecodeError) as e:
190195
self.csv_file = default_config['csv_path']
191196
self.schedule_frequency = default_config['schedule_frequency']
@@ -205,6 +210,8 @@ def load_config(self):
205210
self.wasabi_bucket = default_config['wasabi_bucket']
206211
self.wasabi_region = default_config['wasabi_region']
207212
self.wasabi_enabled = default_config['wasabi_enabled']
213+
self.verify_ssl = default_config['verify_ssl']
214+
self.max_backups = default_config['max_backups']
208215
if isinstance(e, json.JSONDecodeError):
209216
logging.warning("Configuration file is corrupted, using default settings")
210217
else:
@@ -285,7 +292,9 @@ def save_config(self, switch_name=None, ip=None, config=None):
285292
'wasabi_secret_key': self._encrypt(self.wasabi_secret_key),
286293
'wasabi_bucket': self.wasabi_bucket,
287294
'wasabi_region': self.wasabi_region,
288-
'wasabi_enabled': self.wasabi_enabled
295+
'wasabi_enabled': self.wasabi_enabled,
296+
'verify_ssl': self.verify_ssl,
297+
'max_backups': self.max_backups
289298
}
290299
try:
291300
with open(self.config_file, 'w') as f:
@@ -384,6 +393,35 @@ def setup_gui(self):
384393

385394
ttk.Separator(left_column, orient="horizontal").pack(fill="x", pady=5)
386395

396+
# Advanced settings frame for configurable options (timeout, max_backups, SSL verify)
397+
adv_frame = ttk.LabelFrame(left_column, text="Advanced Settings")
398+
adv_frame.pack(fill="x", pady=10)
399+
adv_sub = ttk.Frame(adv_frame)
400+
adv_sub.pack(expand=True, padx=10, pady=5)
401+
402+
self.verify_ssl_var = tk.BooleanVar(value=self.verify_ssl)
403+
verify_toggle = ttk.Checkbutton(adv_sub, text="Verify SSL certificates (recommended)", variable=self.verify_ssl_var, command=self.toggle_verify_ssl)
404+
verify_toggle.pack(pady=2, anchor="w")
405+
406+
timeout_frame = ttk.Frame(adv_sub)
407+
timeout_frame.pack(fill="x", pady=2)
408+
ttk.Label(timeout_frame, text="Timeout (s):").pack(side="left", padx=3)
409+
self.timeout_entry = ttk.Entry(timeout_frame, width=8)
410+
self.timeout_entry.insert(0, str(self.timeout))
411+
self.timeout_entry.pack(side="left", padx=3)
412+
413+
maxb_frame = ttk.Frame(adv_sub)
414+
maxb_frame.pack(fill="x", pady=2)
415+
ttk.Label(maxb_frame, text="Max backups per switch:").pack(side="left", padx=3)
416+
self.max_backups_entry = ttk.Entry(maxb_frame, width=8)
417+
self.max_backups_entry.insert(0, str(self.max_backups))
418+
self.max_backups_entry.pack(side="left", padx=3)
419+
420+
adv_save = ttk.Button(adv_sub, text="Save Settings", command=self.save_advanced_settings)
421+
adv_save.pack(pady=5)
422+
423+
ttk.Separator(left_column, orient="horizontal").pack(fill="x", pady=5)
424+
387425
control_frame = ttk.LabelFrame(left_column, text="Manual Backup")
388426
control_frame.pack(fill="x", pady=10)
389427
run_backup_button = ttk.Button(control_frame, text="Run Now", command=self.manual_backup)
@@ -586,14 +624,44 @@ def toggle_schedule(self):
586624
logging.info("Schedule disabled")
587625
self.save_config()
588626

627+
def toggle_verify_ssl(self):
628+
self.verify_ssl = self.verify_ssl_var.get()
629+
self.save_config()
630+
if not self.verify_ssl:
631+
self._update_gui(lambda: messagebox.showwarning(
632+
"Security Warning",
633+
"SSL verification is DISABLED. This exposes credentials and config data to interception on untrusted networks. "
634+
"Only disable for lab switches with self-signed certs. Enable for production use."
635+
))
636+
logging.info(f"SSL verification {'enabled' if self.verify_ssl else 'disabled'}")
637+
638+
def save_advanced_settings(self):
639+
try:
640+
self.timeout = int(self.timeout_entry.get())
641+
if self.timeout < 5 or self.timeout > 120:
642+
raise ValueError("Timeout must be 5-120 seconds")
643+
self.max_backups = int(self.max_backups_entry.get())
644+
if self.max_backups < 1 or self.max_backups > 20:
645+
raise ValueError("Max backups must be 1-20")
646+
self.verify_ssl = self.verify_ssl_var.get()
647+
self.save_config()
648+
messagebox.showinfo("Success", "Advanced settings saved")
649+
logging.info(f"Advanced settings updated: timeout={self.timeout}, max_backups={self.max_backups}, verify_ssl={self.verify_ssl}")
650+
except ValueError as ve:
651+
messagebox.showerror("Invalid Input", str(ve))
652+
except Exception as e:
653+
logging.error(f"Failed to save advanced settings: {str(e)}")
654+
messagebox.showerror("Error", "Failed to save settings")
655+
589656
def get_switch_config(self, ip, username, password):
590657
max_retries = 3
591658
retry_delay = 5
592659
session = requests.Session()
593660
config_text = None
661+
verify = self.verify_ssl
594662

595663
try:
596-
response = requests.get(f"https://{ip}", timeout=5, verify=False)
664+
response = requests.get(f"https://{ip}", timeout=5, verify=verify)
597665
logging.info(f"Connectivity test to {ip}: {response.status_code}")
598666
except requests.exceptions.RequestException as e:
599667
logging.error(f"Connectivity test to {ip} failed: {str(e)}")
@@ -603,17 +671,26 @@ def get_switch_config(self, ip, username, password):
603671
logged_in = False
604672
try:
605673
login_url = f"https://{ip}/rest/v10.04/login"
606-
login_response = session.post(login_url, data={"username": username, "password": password}, verify=False, timeout=self.timeout)
674+
login_response = session.post(login_url, data={"username": username, "password": password}, verify=verify, timeout=self.timeout)
607675
login_response.raise_for_status()
608676
logged_in = True
609677
logging.info(f"Login successful for {ip} with API v10.04")
610678

611679
config_url = f"https://{ip}/rest/v10.04/configs/running-config"
612-
config_response = session.get(config_url, headers={"Accept": "text/plain"}, verify=False, timeout=self.timeout)
680+
config_response = session.get(config_url, headers={"Accept": "text/plain"}, verify=verify, timeout=self.timeout)
613681
config_response.raise_for_status()
614682
config_text = config_response.text
615683
logging.info(f"Retrieved config from {ip} with API v10.04")
616684
break
685+
except requests.exceptions.HTTPError as e:
686+
# More granular error reporting for API responses
687+
status = e.response.status_code if e.response else "unknown"
688+
text = e.response.text[:200] if e.response else ""
689+
logging.error(f"HTTP error {status} from {ip}: {text}")
690+
if attempt < max_retries - 1:
691+
time.sleep(retry_delay)
692+
continue
693+
return None
617694
except requests.exceptions.RequestException as e:
618695
if attempt < max_retries - 1:
619696
logging.warning(f"Attempt {attempt + 1} failed for {ip}: {str(e)}. Retrying...")
@@ -625,7 +702,7 @@ def get_switch_config(self, ip, username, password):
625702
if logged_in:
626703
try:
627704
logout_url = f"https://{ip}/rest/v10.04/logout"
628-
session.post(logout_url, verify=False, timeout=self.timeout)
705+
session.post(logout_url, verify=verify, timeout=self.timeout)
629706
logging.info(f"Logged out from {ip}")
630707
except requests.exceptions.RequestException as e:
631708
logging.error(f"Failed to logout from {ip}: {str(e)}")

0 commit comments

Comments
 (0)