feat: backup and restore (vault + mysql)#876
Conversation
50b43d1 to
a5e8558
Compare
Signed-off-by: Deezzir <yurii.kondrakov@canonical.com>
Signed-off-by: Deezzir <yurii.kondrakov@canonical.com>
Signed-off-by: Deezzir <yurii.kondrakov@canonical.com>
Signed-off-by: Deezzir <yurii.kondrakov@canonical.com>
Signed-off-by: Deezzir <yurii.kondrakov@canonical.com>
c03c37b to
253ec99
Compare
Signed-off-by: Deezzir <yurii.kondrakov@canonical.com>
6d10439 to
63907a8
Compare
Signed-off-by: Deezzir <yurii.kondrakov@canonical.com>
691aaa8 to
141d379
Compare
Signed-off-by: Deezzir <yurii.kondrakov@canonical.com>
141d379 to
e33a28b
Compare
Signed-off-by: Deezzir <yurii.kondrakov@canonical.com>
0aab721 to
7338fbe
Compare
| params[self.component.backup_id_param] = latest | ||
|
|
||
| try: | ||
| tenacity.Retrying( |
There was a problem hiding this comment.
I found a race condition during a MySQL component restore. After the corresponding control-plane service is paused, there is a brief window during which MySQL may have dangling client connections, so the restore action will fail to close them. Having a retry here solves the issue, but I would like to come up with something more robust and reliable. Open to suggestions
There was a problem hiding this comment.
issue: I find this aggressive retry policy a bit concerning.
Was this tested with applications unpaused?
Signed-off-by: Deezzir <yurii.kondrakov@canonical.com>
c1983b3 to
434862a
Compare
gboutry
left a comment
There was a problem hiding this comment.
I think this PR needs another pass with a more holistic view of the backup and restore workflows.
There is a lot of indirection, which makes the operational sequence and its safety properties difficult to reason about. In particular, BackupComponent does not appear to simplify the implementation: component-specific behavior still leaks into generic steps, especially around action parameters, point-in-time restore, pause/resume coordination, and failure recovery.
The force handling illustrates the problem. It is propagated through several layers without a clear definition of what it is supposed to bypass. In some paths it has no effect, while in others it becomes an action parameter even when the target charm does not support that parameter. This makes --force both unreliable and capable of breaking otherwise valid operations.
Before adding more fixes, I think we should simplify the design around explicit MySQL and Vault workflows, define the guarantees for backup, restore, rollback, and force behavior, and then test those workflows against the actual charm action contracts and supported database topologies. The current mock-heavy tests pass while still encoding assumptions that are invalid with Juju.
| params[self.component.backup_id_param] = latest | ||
|
|
||
| try: | ||
| tenacity.Retrying( |
There was a problem hiding this comment.
issue: I find this aggressive retry policy a bit concerning.
Was this tested with applications unpaused?
| return sorted(successful, key=lambda b: b.backup_id)[-1].backup_id | ||
|
|
||
|
|
||
| def _api_app_for_mysql(app_name: str) -> str: |
There was a problem hiding this comment.
The default MySQL topology is single MySQL app, and if we follow this Pull request, then, to which application is mysql tied to?
|
|
||
| return [ | ||
| _PauseAppStep(jhelper, api_app, force=force, timeout=timeout, model=model), | ||
| _ScaleAppStep( |
There was a problem hiding this comment.
I don't see in the official docs where it's advised to scale mysql down to 1: https://canonical.com/data/mysql/docs/8.0/how-to/back-up-and-restore/restore-a-backup/
Anyway, won't that leave the PVCs on the host, and just provoke more juju hooks churn?
What are we trying to achieve here?
There was a problem hiding this comment.
In the docs, it explicitly says to scale the replicas to 1 before the restore
There was a problem hiding this comment.
Uh, you're right. And there's no conflict when it's scaled back up to 3 with the PVCs?
There was a problem hiding this comment.
No, I guess it just reattaches them back
There was a problem hiding this comment.
Yeah, but then we have "old" pvc on 2 units, and restored PVC 1 unit. I really would like some input on DPE, just to ensure it's a case they have considered.
There was a problem hiding this comment.
I think the behaviour is the following: K8s preserves the old PVCs for the scaled-down units. During the restore, the PRIMARY cluster member rebuilds the database on its storage. After the scale-back, the SECONDARY rebuilds its copies on the old PVCs, replacing the stale data on the existing storage using the MySQL Clone Plugin from the fresh snapshot.
| super().__init__("Restore app", f"Restoring {target.app}") | ||
| self.jhelper = jhelper | ||
| self.target = target | ||
| self.restore_to_time = restore_to_time |
There was a problem hiding this comment.
I feel like the restore_to_time is always None, why include it then?
Any reason we're not looking for PITR recovery mechanism?
| return app_name | ||
|
|
||
|
|
||
| def _current_scale(jhelper: JujuHelper, app: str, model: str) -> int: |
There was a problem hiding this comment.
If we can't figure out the scale of the application, this means juju is in a bad state, and we should not proceed with this operation. We should fail the operation.
| component=target.component, | ||
| backup=backup, | ||
| ) | ||
| except Exception as e: |
There was a problem hiding this comment.
This exception catch is a bit too broad, can we narrow it down?
| return backups | ||
|
|
||
|
|
||
| def _parse_backup(action_result: dict) -> BackupOutcome | None: |
There was a problem hiding this comment.
This allows for None, and we don't gate on that result later, is that expected?
I'd surmise either we've successfully created a backup, or it's a failure?
| if component is None: | ||
| continue | ||
| for app in apps: | ||
| target = component.resolve_backup_target( |
There was a problem hiding this comment.
Isn't it concerning that a discovered app for backup is gone once we try to resolve the target?
| self.scale = scale | ||
| self.timeout = timeout | ||
| self.model = model | ||
| self.force = force |
| def run(self, context: StepContext) -> Result: | ||
| """Dispatch the backup action, recording the outcome on ``self.result``.""" | ||
| target = self.target | ||
| params: dict[str, str | bool] = {"force": True} if self.force else {} |
There was a problem hiding this comment.
There was a problem hiding this comment.
What I found is that Juju will ignore provided parameters if they are not defined in the action, so it is safe to pass
There was a problem hiding this comment.
Is that documented behaviour? I'd rather not depend on an implementation detail that's not properly documented by the juju team if possible.
| # Public component registry | ||
| # --------------------------------------------------------------------------- | ||
| BACKUP_COMPONENTS: list[BackupComponent] = [ | ||
| BackupComponent( |
There was a problem hiding this comment.
I think I'd rather have subclasses VaultBackupComponent and so on, rather than generic object with helper functions. This would make the whole ordeal much better to follow.
Signed-off-by: Deezzir <yurii.kondrakov@canonical.com>
|
@gboutry I've refactored the logic:
|
Overview
sunbeam backup,sunbeam list-backups, andsunbeam restorecommands for stateful workloads in the OpenStack model.BackupComponent) so command flow is generic while component specifics (target resolution, parse/list/restore plans) stay in one registry.Automation
In addition to the new commands, I am working on an experimental feature to automate the deployment and configuration of the
s3-integratorcharm for each MySQL and Vault component detected in the cluster. The branch is currently based on the current changes.See: #890
The feature, at a minimum, deploys the
s3-integratorfor each target application, but the operator can choose to interactively set up the S3 location (single for all integrators).Prerequisites
The current changes are blocked by the
sunbeam-charmsfeature introduced in the PR, adding thepause/resumeactions for the control plane services, likekeystone,cinder, etc. The feature allows stopping the control-plane service's communication with the corresponding databases, so the restore can complete without concurrent writes.QA
The new commands were validated end-to-end on a new Sunbeam Cluster (MAAS) on the personal SE-Cloud.
Demo
Backup
List Backups
Restore