Commit d21fae6
fix: await pre-restore safety backup and forward password on snapshot restore (#1684)
* fix: await pre-restore safety backup and forward password on snapshot restore
ha_manage_backup(scope=snapshot, action=restore) created a pre-restore
safety backup via backup/generate and issued backup/restore back-to-back
without waiting. HA's backup/generate returns once the job is initiated,
not finished, and the backup manager rejects any new operation while a
backup runs ("Backup manager busy: create_backup"), so the restore
collided with the safety backup the same call had just started – a
self-induced deadlock. Each retry spawned another safety backup and
failed the same way; the restore never ran.
Await the safety backup through the existing _poll_backup_completion
helper (the same completion poll create_backup already uses) before
issuing backup/restore. Also forward the already-fetched default backup
password into restore_params so protected (encrypted) backups decrypt on
restore; HA's backup/restore schema types password as str, so it is only
included when a default password is available.
Adds regression tests pinning the await-before-restore ordering and the
password forwarding/omission.
Closes #1681
* test: stub safety-backup poll in restore warnings-shape test
The await added to _create_safety_backup makes restore_backup poll
backup/info for safety-backup completion. test_backup_restore_warnings_shape
scripted send_command as a fixed list without that poll, so the list ran
dry (StopAsyncIteration) in CI. Patch _poll_backup_completion so the test
stays focused on its warnings-list shape contract; the poll itself is
covered by test_backup_restore.
* fix: reconcile restore params with target backup and size safety-backup poll
Address the four items from the PR #1684 maintainer review. All four key off
the target backup's own backup/info entry, which the restore path previously
ignored (it only checked existence), so capture the matched entry once and
derive from it.
- Password is now forwarded only for a protected target. `password` is HA's
default create_backup.password, independent of whether the target backup is
encrypted; HA validates it against the target unconditionally and rejects a
password on an unprotected backup ("Invalid password for backup" ->
IncorrectPasswordError). Gate on the target's `protected` flag so an
unprotected snapshot still restores on a default-password instance.
- restore_database is reconciled against the target's `database_included`.
When restoring Home Assistant, Supervisor requires the two to match and
otherwise raises "Restore database must match backup"; the value is derived
from the target (falling back to the caller's request only when the field is
absent) and an override is surfaced as a warning.
- The safety backup polls with a dedicated _SAFETY_BACKUP_MAX_WAIT_S (1800s)
instead of the 300s fast-backup constant. The safety backup is a full backup
(include_database, all add-ons on Supervised); a multi-GB full backup on
constrained hardware can exceed the fast-backup window, time out, abort the
restore, and trigger a retry that spawns another full backup (#1681).
- The safety-backup completion poll's late-completion warnings are returned to
the caller and folded into the restore response's top-level warnings list,
instead of being discarded.
Expands the #1681 restore regression suite to cover the unprotected-target,
DB-included-target, and late-warning paths.
* fix: read target protected per-agent and gate db reconcile to Supervised
Round-3 review fixes for the snapshot-restore reconciliation (#1681).
1. Password gate read a non-existent top-level `protected`. HA's `backup/info`
returns ManagerBackup entries where `protected` is a per-agent field
(AgentBackupStatus, under `agents[<agent_id>]`), not top-level – only
`database_included` is inherited top-level from BaseBackup. So
`matched.get("protected")` was always None against real HA and the password
was never forwarded, re-breaking decryption of protected backups (the exact
#1681 case). A backup is encrypted as a whole, so a new `_backup_protected`
helper derives the flag from the agents map (any agent reporting protected),
shared by both the restore gate and `_summarize_backup` so the two reads
can't diverge and a non-dict agents map can't raise. `_summarize_backup` had
the same latent top-level assumption and is fixed by the same helper.
2. `restore_database` reconciliation ran unconditionally, but the
"Restore database must match backup" constraint is Supervisor-only
(SupervisorBackupReaderWriter). HA Core's CoreBackupReaderWriter writes the
caller's value verbatim, so overriding it on Core silently discarded an
explicit request for no HA-side reason. Gate the override on
`local_agent == "hassio.local"`; narrow the comment, warning, and docstring
to name Supervisor as the source of the requirement.
3. Tests now match HA's real `backup/info` shape (`protected` nested under
`agents`), so the finding-1 regression is visible to CI. Added: direct
`_backup_protected` coverage, an any-agent-protected restore case, a
Core-honours-caller-value case for `restore_database`, safety-backup
assertions in the unprotected-target test, and a failed-restore path
asserting ToolError with backup_id context.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>1 parent 90e6229 commit d21fae6
4 files changed
Lines changed: 686 additions & 39 deletions
File tree
- src/ha_mcp/tools
- tests/src/unit
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
52 | 52 | | |
53 | 53 | | |
54 | 54 | | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
55 | 61 | | |
56 | 62 | | |
57 | 63 | | |
| |||
540 | 546 | | |
541 | 547 | | |
542 | 548 | | |
543 | | - | |
544 | | - | |
| 549 | + | |
| 550 | + | |
545 | 551 | | |
546 | 552 | | |
547 | 553 | | |
548 | 554 | | |
549 | | - | |
550 | | - | |
| 555 | + | |
| 556 | + | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
551 | 563 | | |
552 | 564 | | |
553 | | - | |
| 565 | + | |
554 | 566 | | |
555 | 567 | | |
556 | 568 | | |
| |||
579 | 591 | | |
580 | 592 | | |
581 | 593 | | |
582 | | - | |
583 | | - | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
| 604 | + | |
| 605 | + | |
| 606 | + | |
| 607 | + | |
| 608 | + | |
| 609 | + | |
| 610 | + | |
| 611 | + | |
| 612 | + | |
| 613 | + | |
| 614 | + | |
| 615 | + | |
| 616 | + | |
| 617 | + | |
| 618 | + | |
| 619 | + | |
| 620 | + | |
| 621 | + | |
| 622 | + | |
| 623 | + | |
| 624 | + | |
| 625 | + | |
| 626 | + | |
| 627 | + | |
| 628 | + | |
| 629 | + | |
| 630 | + | |
| 631 | + | |
| 632 | + | |
584 | 633 | | |
585 | 634 | | |
586 | 635 | | |
| |||
594 | 643 | | |
595 | 644 | | |
596 | 645 | | |
597 | | - | |
| 646 | + | |
| 647 | + | |
| 648 | + | |
| 649 | + | |
| 650 | + | |
| 651 | + | |
598 | 652 | | |
599 | 653 | | |
600 | 654 | | |
| |||
627 | 681 | | |
628 | 682 | | |
629 | 683 | | |
630 | | - | |
| 684 | + | |
631 | 685 | | |
632 | | - | |
| 686 | + | |
633 | 687 | | |
634 | 688 | | |
635 | 689 | | |
| |||
655 | 709 | | |
656 | 710 | | |
657 | 711 | | |
658 | | - | |
| 712 | + | |
| 713 | + | |
| 714 | + | |
| 715 | + | |
| 716 | + | |
| 717 | + | |
| 718 | + | |
| 719 | + | |
| 720 | + | |
| 721 | + | |
| 722 | + | |
| 723 | + | |
| 724 | + | |
| 725 | + | |
| 726 | + | |
| 727 | + | |
| 728 | + | |
| 729 | + | |
| 730 | + | |
| 731 | + | |
| 732 | + | |
| 733 | + | |
| 734 | + | |
| 735 | + | |
| 736 | + | |
| 737 | + | |
| 738 | + | |
659 | 739 | | |
660 | 740 | | |
661 | | - | |
| 741 | + | |
662 | 742 | | |
663 | 743 | | |
664 | | - | |
| 744 | + | |
665 | 745 | | |
666 | 746 | | |
667 | 747 | | |
668 | 748 | | |
| 749 | + | |
| 750 | + | |
| 751 | + | |
| 752 | + | |
| 753 | + | |
| 754 | + | |
| 755 | + | |
| 756 | + | |
| 757 | + | |
| 758 | + | |
| 759 | + | |
| 760 | + | |
| 761 | + | |
| 762 | + | |
669 | 763 | | |
670 | 764 | | |
671 | 765 | | |
| |||
678 | 772 | | |
679 | 773 | | |
680 | 774 | | |
| 775 | + | |
| 776 | + | |
| 777 | + | |
| 778 | + | |
| 779 | + | |
| 780 | + | |
| 781 | + | |
| 782 | + | |
| 783 | + | |
| 784 | + | |
| 785 | + | |
| 786 | + | |
681 | 787 | | |
682 | 788 | | |
683 | 789 | | |
| |||
697 | 803 | | |
698 | 804 | | |
699 | 805 | | |
700 | | - | |
| 806 | + | |
701 | 807 | | |
702 | 808 | | |
703 | 809 | | |
| |||
761 | 867 | | |
762 | 868 | | |
763 | 869 | | |
764 | | - | |
| 870 | + | |
| 871 | + | |
765 | 872 | | |
766 | 873 | | |
767 | 874 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| 14 | + | |
14 | 15 | | |
15 | 16 | | |
16 | 17 | | |
17 | 18 | | |
18 | 19 | | |
19 | 20 | | |
20 | 21 | | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
21 | 49 | | |
22 | 50 | | |
23 | 51 | | |
| |||
157 | 185 | | |
158 | 186 | | |
159 | 187 | | |
160 | | - | |
161 | | - | |
162 | | - | |
163 | | - | |
164 | | - | |
165 | | - | |
166 | | - | |
167 | | - | |
168 | | - | |
169 | 188 | | |
170 | 189 | | |
171 | 190 | | |
| |||
175 | 194 | | |
176 | 195 | | |
177 | 196 | | |
178 | | - | |
179 | | - | |
180 | | - | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
181 | 208 | | |
182 | 209 | | |
183 | 210 | | |
| |||
200 | 227 | | |
201 | 228 | | |
202 | 229 | | |
203 | | - | |
204 | 230 | | |
205 | 231 | | |
206 | 232 | | |
207 | 233 | | |
| 234 | + | |
208 | 235 | | |
209 | | - | |
210 | | - | |
| 236 | + | |
| 237 | + | |
211 | 238 | | |
212 | 239 | | |
213 | 240 | | |
| |||
0 commit comments