Conversation
There was a problem hiding this comment.
Pull request overview
This pull request adds router restart capability to the internet speed monitoring service. The change introduces automatic FRITZ!Box router restart functionality when internet speeds fall below configured thresholds.
Key Changes
- Renamed
SpeedTest.pytospeed_test.pyand refactored for package structure - Added
RouterRestartManagerfor automatic router restart based on speed thresholds - Added
PrometheusManagerfor managing Prometheus server lifecycle - Introduced YAML-based configuration system replacing command-line arguments
- Added comprehensive test coverage for new functionality
Reviewed changes
Copilot reviewed 15 out of 16 changed files in this pull request and generated 18 comments.
Show a summary per file
| File | Description |
|---|---|
| network_speed/uv.lock | Added dependencies: certifi, charset-normalizer, fritzconnection, idna, pyyaml, requests, urllib3 |
| network_speed/src/speed_test.py | New main application with YAML config support, router restart integration, and WSGI compatibility |
| network_speed/src/router_restart.py | New router management module with FritzBox integration and metrics tracking |
| network_speed/src/prometheus_manager.py | New Prometheus lifecycle management module |
| network_speed/src/init.py | Package marker for proper Python package structure |
| network_speed/tests/*.py | Comprehensive test suite for all new functionality |
| network_speed/config.yaml | New YAML configuration file replacing CLI arguments |
| network_speed/pyproject.toml | Updated entry point from SpeedTest to speed_test, added new dependencies |
| network_speed/README.md | Updated documentation reflecting new configuration system and features |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| manager1 = RouterRestartManager(config) | ||
| manager2 = RouterRestartManager(config) | ||
| manager3 = RouterRestartManager(config) |
There was a problem hiding this comment.
Variable manager1 is not used.
| manager1 = RouterRestartManager(config) | |
| manager2 = RouterRestartManager(config) | |
| manager3 = RouterRestartManager(config) | |
| for _ in range(3): | |
| RouterRestartManager(config) |
| manager1 = RouterRestartManager(config) | ||
| manager2 = RouterRestartManager(config) | ||
| manager3 = RouterRestartManager(config) | ||
|
|
||
| # Count file handlers after | ||
| final_file_handlers = [ |
There was a problem hiding this comment.
Variable manager2 is not used.
| manager1 = RouterRestartManager(config) | |
| manager2 = RouterRestartManager(config) | |
| manager3 = RouterRestartManager(config) | |
| # Count file handlers after | |
| final_file_handlers = [ | |
| for _ in range(3): | |
| RouterRestartManager(config) | |
| # Count file handlers after | |
| final_file_handlers = [ | |
| final_file_handlers = [ |
| manager1 = RouterRestartManager(config) | ||
| manager2 = RouterRestartManager(config) | ||
| manager3 = RouterRestartManager(config) |
There was a problem hiding this comment.
Variable manager3 is not used.
| manager1 = RouterRestartManager(config) | |
| manager2 = RouterRestartManager(config) | |
| manager3 = RouterRestartManager(config) | |
| for _ in range(3): | |
| RouterRestartManager(config) |
| "logging": {"enabled": False} | ||
| }) | ||
|
|
||
| manager = RouterRestartManager(config) |
There was a problem hiding this comment.
Variable manager is not used.
| manager = RouterRestartManager(config) | |
| RouterRestartManager(config) |
| # Metric line: name{labels} value or name value | ||
| parts = line.split(' ') | ||
| self.assertEqual(len(parts), 2, f"Invalid metric line: {line}") | ||
| metric_name_with_labels = parts[0] |
There was a problem hiding this comment.
Variable metric_name_with_labels is not used.
| metric_name_with_labels = parts[0] |
| """Tests for PrometheusManager startup/shutdown behavior.""" | ||
|
|
||
| import unittest | ||
| from unittest.mock import Mock, patch, MagicMock |
There was a problem hiding this comment.
Import of 'MagicMock' is not used.
| from unittest.mock import Mock, patch, MagicMock | |
| from unittest.mock import Mock, patch |
| """Tests for RouterRestartManager including config resolution, state handling, and time windows.""" | ||
|
|
||
| import unittest | ||
| from unittest.mock import Mock, patch, MagicMock, mock_open |
There was a problem hiding this comment.
Import of 'MagicMock' is not used.
Import of 'mock_open' is not used.
| from unittest.mock import Mock, patch, MagicMock, mock_open | |
| from unittest.mock import Mock, patch |
| """Tests for WSGI mode where main() is never called.""" | ||
|
|
||
| import unittest | ||
| from unittest.mock import Mock, patch, MagicMock |
There was a problem hiding this comment.
Import of 'MagicMock' is not used.
| from unittest.mock import Mock, patch, MagicMock | |
| from unittest.mock import Mock, patch |
| logging.error(f"Error stopping Prometheus: {e}") | ||
| try: | ||
| self._process.kill() | ||
| except: |
There was a problem hiding this comment.
Except block directly handles BaseException.
| except: | |
| except Exception: |
| logging.error(f"Error stopping Prometheus: {e}") | ||
| try: | ||
| self._process.kill() | ||
| except: |
There was a problem hiding this comment.
'except' clause does nothing but pass and there is no explanatory comment.
| except: | |
| except Exception: | |
| # Intentionally ignore errors while forcefully terminating the process during cleanup. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 16 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def _get_fritzbox_password(self): | ||
| """Retrieve FRITZ!Box password from 1Password.""" | ||
| try: | ||
| result = subprocess.run( | ||
| ["op", "read", self.onepassword_ref], | ||
| capture_output=True, | ||
| text=True, | ||
| check=True | ||
| ) | ||
| return result.stdout.strip() | ||
| except subprocess.CalledProcessError as e: | ||
| logging.error(f"Failed to retrieve password from 1Password: {e}") | ||
| raise |
There was a problem hiding this comment.
Password retrieval from 1Password using subprocess without input validation could be a security risk. Consider adding validation for the onepassword_ref format to prevent command injection, even though the op CLI should handle this safely.
| def _restart_fritzbox(self): | ||
| """Restart the FRITZ!Box router.""" | ||
| try: | ||
| password = self._get_fritzbox_password() | ||
|
|
||
| logging.info(f"Connecting to FRITZ!Box at {self.fritzbox_ip}...") | ||
| fc = FritzConnection( | ||
| address=self.fritzbox_ip, | ||
| user=self.fritzbox_username if self.fritzbox_username else None, | ||
| password=password | ||
| ) | ||
|
|
||
| logging.info("Sending reboot command to FRITZ!Box...") | ||
| fc.call_action("DeviceConfig1", "Reboot") | ||
|
|
||
| logging.info("FRITZ!Box reboot initiated successfully") | ||
|
|
||
| # Record successful restart in instance metrics | ||
| with self._metrics_lock: | ||
| self._restart_total += 1 | ||
| self._restart_last_timestamp = datetime.now().timestamp() | ||
|
|
||
| return True | ||
|
|
||
| except Exception as e: | ||
| logging.error(f"Failed to restart FRITZ!Box: {e}") | ||
|
|
||
| # Record failed restart attempt in instance metrics | ||
| with self._metrics_lock: | ||
| self._restart_failures_total += 1 | ||
|
|
||
| return False |
There was a problem hiding this comment.
The password retrieved from 1Password is logged in case of errors and passed to FritzConnection. While the password itself isn't logged directly, ensure that any exception messages from FritzConnection don't expose the password in traceback information.
| "logging": {"enabled": False} | ||
| }) | ||
|
|
||
| manager = RouterRestartManager(config) |
There was a problem hiding this comment.
Variable manager is not used.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 16 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| try: | ||
| result = subprocess.run( | ||
| ["op", "read", self.onepassword_ref], | ||
| capture_output=True, | ||
| text=True, | ||
| check=True | ||
| ) |
There was a problem hiding this comment.
The subprocess call passes user-configurable input (self.onepassword_ref) to a shell command. While there's validation via _validate_onepassword_ref(), the regex pattern allows spaces and backslashes which could potentially be exploited. Consider using a more restrictive pattern or additional sanitization to prevent command injection risks.
network_speed/src/router_restart.py
Outdated
|
|
||
| def _validate_onepassword_ref(self): | ||
| """Validate the 1Password reference format to prevent unsafe inputs.""" | ||
| pattern = r"^op://[A-Za-z0-9 ._\\-]+/[A-Za-z0-9 ._\\-]+/[A-Za-z0-9 ._\\-]+$" |
There was a problem hiding this comment.
The validation regex on line 243 allows backslashes in the 1Password reference format, which is unusual and potentially risky. The pattern [A-Za-z0-9 ._\\-]+ includes escaped backslash \\. Consider whether backslashes are actually needed in 1Password references, as they could be used in path traversal or injection attacks.
| pattern = r"^op://[A-Za-z0-9 ._\\-]+/[A-Za-z0-9 ._\\-]+/[A-Za-z0-9 ._\\-]+$" | |
| pattern = r"^op://[A-Za-z0-9 ._-]+/[A-Za-z0-9 ._-]+/[A-Za-z0-9 ._-]+$" |
No description provided.