@@ -55,12 +55,14 @@ def is_unit_blocked(self) -> bool:
55
55
from charms .mysql .v0 .mysql import (
56
56
MySQLConfigureInstanceError ,
57
57
MySQLCreateClusterError ,
58
+ MySQLCreateClusterSetError ,
58
59
MySQLDeleteTempBackupDirectoryError ,
59
60
MySQLDeleteTempRestoreDirectoryError ,
60
61
MySQLEmptyDataDirectoryError ,
61
62
MySQLExecuteBackupCommandsError ,
62
63
MySQLGetMemberStateError ,
63
64
MySQLInitializeJujuOperationsTableError ,
65
+ MySQLKillSessionError ,
64
66
MySQLOfflineModeAndHiddenInstanceExistsError ,
65
67
MySQLPrepareBackupForRestoreError ,
66
68
MySQLRescanClusterError ,
@@ -80,13 +82,14 @@ def is_unit_blocked(self) -> bool:
80
82
from ops .charm import ActionEvent
81
83
from ops .framework import Object
82
84
from ops .jujuversion import JujuVersion
83
- from ops .model import ActiveStatus , BlockedStatus
85
+ from ops .model import BlockedStatus , MaintenanceStatus
84
86
85
87
from constants import MYSQL_DATA_DIR
86
88
87
89
logger = logging .getLogger (__name__ )
88
90
89
91
MYSQL_BACKUPS = "mysql-backups"
92
+ S3_INTEGRATOR_RELATION_NAME = "s3-parameters"
90
93
91
94
# The unique Charmhub library identifier, never change it
92
95
LIBID = "183844304be247129572309a5fb1e47c"
@@ -96,7 +99,7 @@ def is_unit_blocked(self) -> bool:
96
99
97
100
# Increment this PATCH version before using `charmcraft publish-lib` or reset
98
101
# to 0 if you are raising the major API version
99
- LIBPATCH = 8
102
+ LIBPATCH = 10
100
103
101
104
102
105
if typing .TYPE_CHECKING :
@@ -117,6 +120,10 @@ def __init__(self, charm: "MySQLOperatorCharm", s3_integrator: S3Requirer) -> No
117
120
self .framework .observe (self .charm .on .restore_action , self ._on_restore )
118
121
119
122
# ------------------ Helpers ------------------
123
+ @property
124
+ def _s3_integrator_relation_exists (self ) -> bool :
125
+ """Returns whether a relation with the s3-integrator exists."""
126
+ return bool (self .model .get_relation (S3_INTEGRATOR_RELATION_NAME ))
120
127
121
128
def _retrieve_s3_parameters (self ) -> Tuple [Dict [str , str ], List [str ]]:
122
129
"""Retrieve S3 parameters from the S3 integrator relation.
@@ -176,11 +183,7 @@ def _upload_logs_to_s3(
176
183
177
184
Returns: bool indicating success
178
185
"""
179
- logs = f"""Stdout:
180
- { stdout }
181
-
182
- Stderr:
183
- { stderr } """
186
+ logs = f"Stdout:\n { stdout } \n \n Stderr:\n { stderr } "
184
187
logger .debug (f"Logs to upload to S3 at location { log_filename } :\n { logs } " )
185
188
186
189
logger .info (
@@ -206,7 +209,7 @@ def _on_list_backups(self, event: ActionEvent) -> None:
206
209
207
210
List backups available to restore by this application.
208
211
"""
209
- if not self .charm . s3_integrator_relation_exists :
212
+ if not self ._s3_integrator_relation_exists :
210
213
event .fail ("Missing relation with S3 integrator charm" )
211
214
return
212
215
@@ -222,7 +225,9 @@ def _on_list_backups(self, event: ActionEvent) -> None:
222
225
event .set_results ({"backups" : self ._format_backups_list (backups )})
223
226
except Exception as e :
224
227
error_message = (
225
- e .message if hasattr (e , "message" ) else "Failed to retrieve backup ids from S3"
228
+ getattr (e , "message" )
229
+ if hasattr (e , "message" )
230
+ else "Failed to retrieve backup ids from S3"
226
231
)
227
232
logger .error (error_message )
228
233
event .fail (error_message )
@@ -233,7 +238,7 @@ def _on_create_backup(self, event: ActionEvent) -> None:
233
238
"""Handle the create backup action."""
234
239
logger .info ("A backup has been requested on unit" )
235
240
236
- if not self .charm . s3_integrator_relation_exists :
241
+ if not self ._s3_integrator_relation_exists :
237
242
logger .error ("Backup failed: missing relation with S3 integrator charm" )
238
243
event .fail ("Missing relation with S3 integrator charm" )
239
244
return
@@ -263,12 +268,13 @@ def _on_create_backup(self, event: ActionEvent) -> None:
263
268
264
269
# Test uploading metadata to S3 to test credentials before backup
265
270
juju_version = JujuVersion .from_environ ()
266
- metadata = f"""Date Backup Requested: { datetime_backup_requested }
267
- Model Name: { self .model .name }
268
- Application Name: { self .model .app .name }
269
- Unit Name: { self .charm .unit .name }
270
- Juju Version: { str (juju_version )}
271
- """
271
+ metadata = (
272
+ f"Date Backup Requested: { datetime_backup_requested } \n "
273
+ f"Model Name: { self .model .name } \n "
274
+ f"Application Name: { self .model .app .name } \n "
275
+ f"Unit Name: { self .charm .unit .name } \n "
276
+ f"Juju Version: { str (juju_version )} \n "
277
+ )
272
278
273
279
if not upload_content_to_s3 (metadata , f"{ backup_path } .metadata" , s3_parameters ):
274
280
logger .error ("Backup failed: Failed to upload metadata to provided S3" )
@@ -313,6 +319,7 @@ def _on_create_backup(self, event: ActionEvent) -> None:
313
319
"backup-id" : datetime_backup_requested ,
314
320
}
315
321
)
322
+ self .charm ._on_update_status (None )
316
323
317
324
def _can_unit_perform_backup (self ) -> Tuple [bool , Optional [str ]]:
318
325
"""Validates whether this unit can perform a backup.
@@ -380,8 +387,9 @@ def _backup(self, backup_path: str, s3_parameters: Dict) -> Tuple[bool, Optional
380
387
Returns: tuple of (success, error_message)
381
388
"""
382
389
try :
390
+ self .charm .unit .status = MaintenanceStatus ("Running backup..." )
383
391
logger .info ("Running the xtrabackup commands" )
384
- stdout = self .charm ._mysql .execute_backup_commands (
392
+ stdout , _ = self .charm ._mysql .execute_backup_commands (
385
393
backup_path ,
386
394
s3_parameters ,
387
395
)
@@ -436,7 +444,7 @@ def _pre_restore_checks(self, event: ActionEvent) -> bool:
436
444
437
445
Returns: a boolean indicating whether restore should be run
438
446
"""
439
- if not self .charm . s3_integrator_relation_exists :
447
+ if not self ._s3_integrator_relation_exists :
440
448
error_message = "Missing relation with S3 integrator charm"
441
449
logger .error (f"Restore failed: { error_message } " )
442
450
event .fail (error_message )
@@ -481,7 +489,7 @@ def _on_restore(self, event: ActionEvent) -> None:
481
489
if not self ._pre_restore_checks (event ):
482
490
return
483
491
484
- backup_id = event .params . get ( "backup-id" ) .strip ().strip ("/" )
492
+ backup_id = event .params [ "backup-id" ] .strip ().strip ("/" )
485
493
logger .info (f"A restore with backup-id { backup_id } has been requested on unit" )
486
494
487
495
# Retrieve and validate missing S3 parameters
@@ -500,6 +508,7 @@ def _on_restore(self, event: ActionEvent) -> None:
500
508
return
501
509
502
510
# Run operations to prepare for the restore
511
+ self .charm .unit .status = MaintenanceStatus ("Running pre-restore operations" )
503
512
success , error_message = self ._pre_restore ()
504
513
if not success :
505
514
logger .error (f"Restore failed: { error_message } " )
@@ -520,6 +529,7 @@ def _on_restore(self, event: ActionEvent) -> None:
520
529
return
521
530
522
531
# Run post-restore operations
532
+ self .charm .unit .status = MaintenanceStatus ("Running post-restore operations" )
523
533
success , error_message = self ._post_restore ()
524
534
if not success :
525
535
logger .error (f"Restore failed: { error_message } " )
@@ -533,22 +543,32 @@ def _on_restore(self, event: ActionEvent) -> None:
533
543
"completed" : "ok" ,
534
544
}
535
545
)
546
+ # update status as soon as possible
547
+ self .charm ._on_update_status (None )
536
548
537
- def _pre_restore (self ) -> Tuple [bool , Optional [ str ] ]:
549
+ def _pre_restore (self ) -> Tuple [bool , str ]:
538
550
"""Perform operations that need to be done before performing a restore.
539
551
540
552
Returns: tuple of (success, error_message)
541
553
"""
554
+ if not self .charm ._mysql .is_mysqld_running ():
555
+ return True , ""
556
+
542
557
try :
558
+ logger .info ("Stopping mysqld before restoring the backup" )
559
+ self .charm ._mysql .kill_client_sessions ()
560
+ self .charm ._mysql .set_instance_offline_mode (True )
543
561
self .charm ._mysql .stop_mysqld ()
562
+ except MySQLKillSessionError :
563
+ return False , "Failed to kill client sessions"
564
+ except MySQLSetInstanceOfflineModeError :
565
+ return False , "Failed to set instance as offline before restoring the backup"
544
566
except MySQLStopMySQLDError :
545
567
return False , "Failed to stop mysqld"
546
568
547
- return True , None
569
+ return True , ""
548
570
549
- def _restore (
550
- self , backup_id : str , s3_parameters : Dict [str , str ]
551
- ) -> Tuple [bool , bool , Optional [str ]]:
571
+ def _restore (self , backup_id : str , s3_parameters : Dict [str , str ]) -> Tuple [bool , bool , str ]:
552
572
"""Run the restore operations.
553
573
554
574
Args:
@@ -558,18 +578,21 @@ def _restore(
558
578
Returns: tuple of (success, recoverable_error, error_message)
559
579
"""
560
580
try :
561
- logger .info ("Running xbcloud get commands to retrieve the backup" )
581
+ logger .info (
582
+ "Running xbcloud get commands to retrieve the backup\n "
583
+ "This operation can take long time depending on backup size and network speed"
584
+ )
585
+ self .charm .unit .status = MaintenanceStatus ("Downloading backup..." )
562
586
stdout , stderr , backup_location = self .charm ._mysql .retrieve_backup_with_xbcloud (
563
587
backup_id ,
564
588
s3_parameters ,
565
589
)
566
- logger .debug (f"Stdout of xbcloud get commands: { stdout } " )
567
- logger .debug (f"Stderr of xbcloud get commands: { stderr } " )
568
590
except MySQLRetrieveBackupWithXBCloudError :
569
591
return False , True , f"Failed to retrieve backup { backup_id } "
570
592
571
593
try :
572
594
logger .info ("Preparing retrieved backup using xtrabackup prepare" )
595
+ self .charm .unit .status = MaintenanceStatus ("Preparing for restore backup..." )
573
596
stdout , stderr = self .charm ._mysql .prepare_backup_for_restore (backup_location )
574
597
logger .debug (f"Stdout of xtrabackup prepare command: { stdout } " )
575
598
logger .debug (f"Stderr of xtrabackup prepare command: { stderr } " )
@@ -583,16 +606,17 @@ def _restore(
583
606
return False , False , "Failed to empty the data directory"
584
607
585
608
try :
609
+ self .charm .unit .status = MaintenanceStatus ("Restoring backup..." )
586
610
logger .info ("Restoring the backup" )
587
611
stdout , stderr = self .charm ._mysql .restore_backup (backup_location )
588
612
logger .debug (f"Stdout of xtrabackup move-back command: { stdout } " )
589
613
logger .debug (f"Stderr of xtrabackup move-back command: { stderr } " )
590
614
except MySQLRestoreBackupError :
591
615
return False , False , f"Failed to restore backup { backup_id } "
592
616
593
- return True , True , None
617
+ return True , True , ""
594
618
595
- def _clean_data_dir_and_start_mysqld (self ) -> Tuple [bool , Optional [ str ] ]:
619
+ def _clean_data_dir_and_start_mysqld (self ) -> Tuple [bool , str ]:
596
620
"""Run idempotent operations run after restoring a backup.
597
621
598
622
Returns tuple of (success, error_message)
@@ -613,9 +637,9 @@ def _clean_data_dir_and_start_mysqld(self) -> Tuple[bool, Optional[str]]:
613
637
except MySQLStartMySQLDError :
614
638
return False , "Failed to start mysqld"
615
639
616
- return True , None
640
+ return True , ""
617
641
618
- def _post_restore (self ) -> Tuple [bool , Optional [ str ] ]:
642
+ def _post_restore (self ) -> Tuple [bool , str ]:
619
643
"""Run operations required after restoring a backup.
620
644
621
645
Returns: tuple of (success, error_message)
@@ -638,24 +662,18 @@ def _post_restore(self) -> Tuple[bool, Optional[str]]:
638
662
logger .info ("Creating cluster on restored node" )
639
663
unit_label = self .charm .unit .name .replace ("/" , "-" )
640
664
self .charm ._mysql .create_cluster (unit_label )
665
+ self .charm ._mysql .create_cluster_set ()
641
666
self .charm ._mysql .initialize_juju_units_operations_table ()
642
667
643
668
self .charm ._mysql .rescan_cluster ()
644
669
645
- logger .info ("Retrieving instance cluster state and role" )
646
- state , role = self .charm ._mysql .get_member_state ()
647
670
except MySQLCreateClusterError :
648
671
return False , "Failed to create InnoDB cluster on restored instance"
672
+ except MySQLCreateClusterSetError :
673
+ return False , "Failed to create InnoDB cluster-set on restored instance"
649
674
except MySQLInitializeJujuOperationsTableError :
650
675
return False , "Failed to initialize the juju operations table"
651
676
except MySQLRescanClusterError :
652
677
return False , "Failed to rescan the cluster"
653
- except MySQLGetMemberStateError :
654
- return False , "Failed to retrieve member state in restored instance"
655
-
656
- self .charm .unit_peer_data ["member-role" ] = role
657
- self .charm .unit_peer_data ["member-state" ] = state
658
678
659
- self .charm .unit .status = ActiveStatus ()
660
-
661
- return True , None
679
+ return True , ""
0 commit comments