Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backup protected status can vary per location #5569

Merged
merged 6 commits into from
Jan 23, 2025
Merged

Conversation

mdegat01
Copy link
Contributor

@mdegat01 mdegat01 commented Jan 22, 2025

Proposed change

Backups can now be easily encrypted and decrypted while streaming and core does this regularly. Therefore two backups that are otherwise identical except one is encrypted and one is not need to be considered the same. And Supervisor should tell clients the protected status of backups across locations.

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New feature (which adds functionality to the supervisor)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Additional information

Checklist

  • The code change is tested and works locally.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • I have followed the development checklist
  • The code has been formatted using Ruff (ruff format supervisor tests)
  • Tests have been added to verify that the new code works.

If API endpoints or add-on configuration are added/changed:

Summary by CodeRabbit

  • New Features

    • Enhanced backup location management with detailed protection status.
    • Added ability to track protected backup locations.
  • Improvements

    • Improved backup information retrieval.
    • More robust handling of backup file paths and encryption status.
    • Centralized password validation for backup locations.
  • Bug Fixes

    • Updated backup location data structure for more accurate tracking.
  • Tests

    • Added new test cases for backup encryption and protection scenarios.
    • Enhanced existing tests to validate new backup location structure.

These changes provide a more comprehensive approach to managing backup locations, ensuring detailed information about backup protection and file paths.

@mdegat01 mdegat01 added missing-documentation Added to pull requests that needs a docs, but none is linked new-feature A new feature need-client-library Added to pull requests that need client library changes but none is linked labels Jan 22, 2025
Copy link
Contributor

coderabbitai bot commented Jan 22, 2025

📝 Walkthrough

Walkthrough

The changes introduce a more comprehensive approach to managing backup locations in the supervisor's backup system. Modifications enhance the data structure for backup locations by adding attributes for file path and protection status. This involves updates across multiple files including backups.py, const.py, backup.py, manager.py, and corresponding test files. The changes aim to provide more detailed information about backup locations, including their protection status and precise file paths, while maintaining the existing functionality of the backup management system.

Changes

File Change Summary
supervisor/api/backups.py - Added ATTR_PATH and ATTR_PROTECTED_LOCATIONS attributes
- Modified _list_backups to include protected locations
- Updated backup_info method to reflect new attributes
supervisor/api/const.py - Added new constant ATTR_PROTECTED_LOCATIONS = "protected_locations"
supervisor/backups/backup.py - Restructured _locations to store path and protection status
- Updated protected and tarfile properties
- Modified all_locations to return enhanced location information
- Updated new and load methods to handle protection status
supervisor/backups/manager.py - Added new method _validate_location_password
- Updated remove and _copy_to_additional_locations methods
- Refactored restore methods to use new validation method
tests/api/test_backups.py - Added tests for mixed encryption backups
- Updated location structure in existing tests
- Added test_protected_backup method
tests/backups/test_backup.py - Added test_consolidate_conflict_varied_encryption test
tests/backups/test_manager.py - Updated assertions to reflect new all_locations structure

Sequence Diagram

sequenceDiagram
    participant BackupManager
    participant Backup
    participant API

    BackupManager->>Backup: Create backup with location details
    Backup-->>BackupManager: Backup object with path and protection status
    BackupManager->>BackupManager: Validate location and password
    BackupManager->>API: Return backup information
    API->>API: Process backup details including protection status
Loading

The sequence diagram illustrates the enhanced flow of backup creation and information retrieval, highlighting the new approach of storing and passing detailed location information including path and protection status.

✨ Finishing Touches
  • 📝 Generate Docstrings (Beta)

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai or @coderabbitai title anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
tests/api/test_backups.py (1)

626-630: Consider checking the return status of assertions

After making assertions about file existence, it's good practice to handle potential exceptions or check the return status to ensure the test behaves as expected even if the files are missing.

While this is a test, adding error handling can make debugging easier if the test fails unexpectedly.

tests/backups/test_backup.py (2)

30-47: Avoid code duplication by utilizing a fixture or helper function

The test test_consolidate_conflict_varied_encryption duplicates code for setting up backups. Consider creating a fixture or a helper function to reduce duplication and enhance maintainability.

Extract the backup setup code into a fixture:

@pytest.fixture
def encrypted_and_unencrypted_backups(coresys, tmp_path):
    enc_tar = Path(copy(get_fixture_path("test_consolidate.tar"), tmp_path))
    enc_backup = Backup(coresys, enc_tar, "test", None)
    await enc_backup.load()

    unc_tar = Path(copy(get_fixture_path("test_consolidate_unc.tar"), tmp_path))
    unc_backup = Backup(coresys, unc_tar, "test", None)
    await unc_backup.load()

    return enc_backup, unc_backup

Then use it in the test:

async def test_consolidate_conflict_varied_encryption(
    coresys: CoreSys, encrypted_and_unencrypted_backups, caplog: pytest.LogCaptureFixture
):
    enc_backup, unc_backup = encrypted_and_unencrypted_backups
    ...

44-47: Improve assertion clarity

The assertion message can be enhanced for better clarity in case of test failure.

Include a message in the assertion:

assert enc_backup.all_locations == {None: {"path": unc_tar, "protected": False}}, "Backup locations did not consolidate as expected"
supervisor/api/const.py (1)

56-56: Maintain alphabetical order of constants

To improve readability and maintainability, consider inserting ATTR_PROTECTED_LOCATIONS in alphabetical order within the list of constants.

Place ATTR_PROTECTED_LOCATIONS after ATTR_PROTECTED (if it exists) or at the appropriate position to keep the list sorted.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 805017e and d6520de.

⛔ Files ignored due to path filters (2)
  • tests/fixtures/test_consolidate.tar is excluded by !**/*.tar
  • tests/fixtures/test_consolidate_unc.tar is excluded by !**/*.tar
📒 Files selected for processing (7)
  • supervisor/api/backups.py (5 hunks)
  • supervisor/api/const.py (1 hunks)
  • supervisor/backups/backup.py (10 hunks)
  • supervisor/backups/manager.py (6 hunks)
  • tests/api/test_backups.py (11 hunks)
  • tests/backups/test_backup.py (2 hunks)
  • tests/backups/test_manager.py (2 hunks)
🔇 Additional comments (18)
tests/api/test_backups.py (2)

952-986: Clarify the handling of protected backups in test_protected_backup

The test creates a protected backup and asserts that protected_locations is [None]. Ensure that the API behavior aligns with the expected outcome, especially when backups exist in multiple locations.

Please confirm that the protected_locations field accurately reflects all locations where the backup is protected. If backups can exist in multiple locations, some protected and some not, the protected_locations should include all relevant locations.


925-950: Ensure test test_backup_mixed_encryption reflects accurate backup locations

The test checks that backup.all_locations includes both None and .cloud_backup locations, but later asserts that body["data"]["backups"][0]["locations"] is [None]. This seems inconsistent.

Please verify that the API correctly reports all backup locations. Run the following script to check the backup locations returned by the API:

This script checks the locations reported for the backup with slug d9c48f8b.

✅ Verification successful

The test case correctly validates the API's security behavior

The test is correctly verifying that when a backup exists in multiple locations with mixed encryption states, the API only exposes the protected location (None) while hiding the unprotected location (.cloud_backup). This appears to be an intentional security feature to prevent exposing information about unprotected backup copies.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify that the backup locations are correctly reported by the API.

# Expected: The backup should report both None and .cloud_backup in its locations.

curl -s http://localhost:8123/backups | jq '.data.backups[] | select(.slug=="d9c48f8b") | .locations'

Length of output: 103


Script:

#!/bin/bash
# Find the API endpoint implementation for backups
ast-grep --pattern 'async def get_backups($$$)' 

# Search for any documentation about backup locations
rg -l "backup.*location" -g "*.md"

# Find similar test cases
rg -l "test_backup.*location" -g "test_*.py"

Length of output: 189


Script:

#!/bin/bash
# Check backup manager tests for location handling
rg -A 5 "all_locations" tests/backups/test_manager.py

# Find API route handler
ast-grep --pattern 'router.get("/backups"$$$)'

# Check the test file content
cat tests/api/test_backups.py | grep -A 10 "async def test_backup.*location"

Length of output: 3209


Script:

#!/bin/bash
# Find the API implementation with a different pattern
ast-grep --pattern 'async def api_backups($$$)'

# Look for the route definition
rg -A 5 '@router.get\("/backups"\)' 

# Check for any security-related comments
rg -B 2 "protected.*locations" 

Length of output: 1081

supervisor/api/backups.py (4)

36-36: LGTM! New attributes added for backup locations.

The addition of ATTR_PATH and ATTR_PROTECTED_LOCATIONS enhances the API's ability to handle backup locations and their protection status.

Also applies to: 58-58


168-172: LGTM! Protected locations list added to backup info.

The list comprehension efficiently filters locations based on their protection status, providing valuable information to API consumers.


244-248: LGTM! Protected locations list added to detailed backup info.

The implementation is consistent with the list in the backup overview.


473-473: LGTM! Updated path access for backup files.

The change correctly uses the new nested dictionary structure to access backup file paths.

supervisor/backups/backup.py (7)

42-42: LGTM! New attributes added for backup locations.

The addition of ATTR_PATH and ATTR_PROTECTED aligns with the changes in the backup location structure.


95-100: LGTM! Enhanced backup location storage.

The new structure stores both path and protection status for each location, improving data organization.


130-130: LGTM! Updated protected property.

The property now correctly accesses the protection status from the new location structure.


207-207: LGTM! Updated path access methods.

The properties correctly handle the new nested dictionary structure for accessing backup paths and locations.

Also applies to: 245-245


261-281: LGTM! Enhanced backup equality comparison.

The comparison now correctly ignores protection status when determining if backups are equal, as current encryption status does not affect equality.


293-306: LGTM! Improved conflict handling in backup consolidation.

The code now properly handles and logs conflicts when consolidating backups from different locations.


335-335: LGTM! Updated protection status handling.

The code correctly sets protection status in the new location structure during backup creation and loading.

Also applies to: 462-464

supervisor/backups/manager.py (4)

15-16: LGTM! New attributes added for backup locations.

The addition of ATTR_PATH and ATTR_PROTECTED aligns with the changes in the backup location structure.


291-291: LGTM! Updated path access in remove method.

The code correctly accesses the backup file path from the new location structure.


345-357: LGTM! Enhanced location handling in copy operations.

The code properly updates the backup locations with both path and protection status after copying.


681-704: LGTM! New validation method for location and password.

The new method centralizes location and password validation logic, improving code organization and reusability.

tests/backups/test_manager.py (1)

1992-1995: LGTM! Updated test assertions for backup locations.

The test assertions correctly reflect the new structure of backup locations, including both path and protection status.

Also applies to: 2012-2015, 2021-2021

tests/api/test_backups.py Show resolved Hide resolved
@agners
Copy link
Member

agners commented Jan 23, 2025

The tests/backups/test_manager.py::test_backup_remove_error test failure actually uncovers a bug in that test in general: The backup hasn't been in the test location at all, this test should have failed even before this change 🙈 .

I've updated the test to actually copy the backup to that mount location as well, and fixed the test accordingly.

@agners agners force-pushed the protected-per-location branch 2 times, most recently from e8b5452 to 7eab589 Compare January 23, 2025 10:31
supervisor/backups/backup.py Outdated Show resolved Hide resolved
@home-assistant home-assistant bot marked this pull request as draft January 23, 2025 11:16
@home-assistant
Copy link

Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍

Learn more about our pull request process.

@agners agners marked this pull request as ready for review January 23, 2025 11:20
@home-assistant home-assistant bot requested a review from agners January 23, 2025 11:20
@@ -263,7 +263,7 @@ def __eq__(self, other: Any) -> bool:

# Compare all fields except ones about protection. Current encryption status does not affect equality
keys = self._data.keys() | other._data.keys()
for k in keys - {ATTR_PROTECTED, ATTR_CRYPTO}:
for k in keys - {ATTR_PROTECTED, ATTR_CRYPTO, ATTR_DOCKER}:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have tests to ensure these attributes are ignored?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a test to confirm an encrypted backup and unencrypted backup can be consolidated, which means those fields were ignored. I didn't have docker in there before so my backup fixtures didn't have that, I'll have to check on that.

Technically for docker we should only be ignoring it if one backup is encrypted and one isn't, otherwise it should be the same. I don't know if we want to deal with that for a field to be deprecated though

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if we want to deal with that for a field to be deprecated though

Yeah let's not bother about that.

But adding docker attributes and make sure they get ignored in the test make sense 👍 .

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I've updated the test fixture files to include a docker repository setting. The test fails without the change above, but with the change merged succeeds 👍 .

@@ -1734,12 +1734,20 @@ async def test_backup_remove_error(
healthy_expected: bool,
):
"""Test removing a backup error."""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did this test start failing?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Essentially, because one of the backup didn't exist before. But previously the test just "created" the location, and now because all_locations it is a dictionary in a dictionary that lead to an error.

A bit more detail in the comment below at #5569 (comment).

Comment on lines 1738 to 1744
if location_name is None:
copy(backup_file, coresys.config.path_backup)
location = None
else:
(mount_dir := coresys.config.path_mounts / location_name).mkdir()
copy(backup_file, mount_dir)
location = coresys.mounts.get(location_name)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of the if-statement, consider adding additional parametrization variable, for the subdirectory and maybe the key to get the location from coresys.mounts.get

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately how to get the location is different for None and mount backups. I've reused existing code now, so that makes the if statement unnecessary.

@mdegat01
Copy link
Contributor Author

The tests/backups/test_manager.py::test_backup_remove_error test failure actually uncovers a bug in that test in general: The backup hasn't been in the test location at all, this test should have failed even before this change 🙈 .

I've updated the test to actually copy the backup to that mount location as well, and fixed the test accordingly.

Wait what was the failure? I didn't think the file not being in that location would cause a failure, I'm pretty sure other tests do that too?

@agners
Copy link
Member

agners commented Jan 23, 2025

Wait what was the failure? I didn't think the file not being in that location would cause a failure, I'm pretty sure other tests do that too?

It is not that the file did not exist which caused the error per se, it was that the location was not in the all_locations property. This made mocking the tar file not work, essentially this would fail (where location_name is test):

    backup.all_locations[location_name]["path"] = (tar_file_mock := MagicMock())

I guess this would have worked too:

    backup.all_locations[location_name] = {}
    backup.all_locations[location_name]["path"] = (tar_file_mock := MagicMock())

But I feel it is cleaner to actually create the backup file, let the code make sure that location_name is in backup.all_locations. Also added that assert:

    assert location_name in backup.all_locations
    backup.all_locations[location_name]["path"] = (tar_file_mock := MagicMock())

@agners agners requested a review from emontnemery January 23, 2025 13:47
@mdegat01 mdegat01 force-pushed the protected-per-location branch from 072b656 to f0cc2d5 Compare January 23, 2025 19:55
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
tests/api/test_backups.py (1)

547-547: ⚠️ Potential issue

Avoid accessing protected member _locations directly.

Direct modification of protected member _locations breaks encapsulation. Consider adding a public method or using dependency injection for testing.

🧹 Nitpick comments (2)
tests/backups/test_backup.py (1)

30-47: LGTM with suggestions for improvement!

The test function effectively validates the consolidation behavior for backups with varied encryption. Consider these improvements:

  • Use more descriptive variable names like encrypted_backup_tar and unencrypted_backup_tar for better readability.
  • Consider parameterizing the test to cover more encryption combinations.
-    enc_tar = Path(copy(get_fixture_path("test_consolidate.tar"), tmp_path))
-    enc_backup = Backup(coresys, enc_tar, "test", None)
+    encrypted_backup_tar = Path(copy(get_fixture_path("test_consolidate.tar"), tmp_path))
+    encrypted_backup = Backup(coresys, encrypted_backup_tar, "test", None)

-    unc_tar = Path(copy(get_fixture_path("test_consolidate_unc.tar"), tmp_path))
-    unc_backup = Backup(coresys, unc_tar, "test", None)
+    unencrypted_backup_tar = Path(copy(get_fixture_path("test_consolidate_unc.tar"), tmp_path))
+    unencrypted_backup = Backup(coresys, unencrypted_backup_tar, "test", None)
tests/backups/test_manager.py (1)

1759-1769: Consider extracting test setup to a fixture.

The test setup logic could be moved to a fixture to improve reusability and reduce code duplication.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 072b656 and f0cc2d5.

⛔ Files ignored due to path filters (2)
  • tests/fixtures/test_consolidate.tar is excluded by !**/*.tar
  • tests/fixtures/test_consolidate_unc.tar is excluded by !**/*.tar
📒 Files selected for processing (7)
  • supervisor/api/backups.py (5 hunks)
  • supervisor/api/const.py (1 hunks)
  • supervisor/backups/backup.py (10 hunks)
  • supervisor/backups/manager.py (6 hunks)
  • tests/api/test_backups.py (11 hunks)
  • tests/backups/test_backup.py (2 hunks)
  • tests/backups/test_manager.py (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • supervisor/api/const.py
  • supervisor/api/backups.py
⏰ Context from checks skipped due to timeout of 90000ms (5)
  • GitHub Check: Run tests Python 3.13.1
  • GitHub Check: Check pylint
  • GitHub Check: Build armv7 supervisor
  • GitHub Check: Build armhf supervisor
  • GitHub Check: Build aarch64 supervisor
🔇 Additional comments (7)
supervisor/backups/backup.py (3)

95-100: LGTM! Well-structured location data model.

The new dictionary structure for _locations improves data organization by keeping related attributes together.


261-281: LGTM! Robust equality comparison with detailed logging.

The equality comparison now correctly handles protection status and provides detailed debug logging for mismatches.


296-306: LGTM! Clear conflict handling with informative logging.

The consolidation logic now provides clear warning messages when conflicts are detected.

supervisor/backups/manager.py (1)

688-711: LGTM! Well-structured validation method.

The new _validate_location_password method effectively centralizes validation logic and provides clear error messages.

tests/api/test_backups.py (2)

927-950: LGTM! Comprehensive test for mixed encryption scenarios.

The test effectively validates backup behavior with different encryption states across locations.


952-986: LGTM! Well-structured test for protected backups.

The test thoroughly validates the protected status of backups and their locations.

tests/backups/test_manager.py (1)

2019-2022: LGTM! Clear assertions for location structure.

The assertions effectively validate the new location structure with both path and protection status.

Also applies to: 2039-2042

@mdegat01 mdegat01 merged commit 61a2101 into main Jan 23, 2025
20 checks passed
@mdegat01 mdegat01 deleted the protected-per-location branch January 23, 2025 20:05
@mdegat01 mdegat01 removed the need-client-library Added to pull requests that need client library changes but none is linked label Jan 23, 2025
@github-actions github-actions bot locked and limited conversation to collaborators Jan 25, 2025
@mdegat01 mdegat01 removed the missing-documentation Added to pull requests that needs a docs, but none is linked label Jan 29, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants