@@ -657,6 +657,12 @@ class CephRGWTest(test_utils.BaseCharmTest):
657657 primary_rgw_unit = 'ceph-radosgw/0'
658658 secondary_rgw_app = 'secondary-ceph-radosgw'
659659 secondary_rgw_unit = 'secondary-ceph-radosgw/0'
660+ cloud_sync_rgw_app = 'cloud-sync-ceph-radosgw'
661+ # These S3 Juju apps are used for the cloud sync tests. They are deployed
662+ # using the following Juju charm: https://charmhub.io/minio-test
663+ # Their purpose is to provide S3 destinations for the cloud sync tests.
664+ cloud_sync_default_s3_app = 's3-default'
665+ cloud_sync_dev_s3_app = 's3-dev'
660666
661667 @classmethod
662668 def setUpClass (cls ):
@@ -685,6 +691,15 @@ def multisite(self):
685691 except KeyError :
686692 return False
687693
694+ @property
695+ def cloud_sync (self ):
696+ """Determine whether Ceph cloud sync is used."""
697+ try :
698+ zaza_model .get_application (self .cloud_sync_rgw_app )
699+ return True
700+ except KeyError :
701+ return False
702+
688703 def get_rgwadmin_cmd_skeleton (self , unit_name ):
689704 """
690705 Get radosgw-admin cmd skeleton with rgw.hostname populated key.
@@ -856,6 +871,35 @@ def get_rgw_endpoint(self, unit_name: str):
856871 except KeyError :
857872 return "http://{}:80" .format (unit_address )
858873
874+ def get_minio_boto3_client (self , app_name : str ):
875+ """Get boto3 client for MinIO application.
876+
877+ :param app_name: MinIO Juju app name.
878+ :type app_name: str
879+ """
880+ leader_unit = zaza_model .get_lead_unit (app_name )
881+ unit_address = zaza_model .get_unit_public_address (
882+ leader_unit ,
883+ self .model_name
884+ )
885+
886+ logging .debug ("Minio Leader Unit: {}, Endpoint: {}" .format (
887+ leader_unit .entity_id , unit_address ))
888+ if unit_address is None :
889+ return None
890+
891+ app_config = zaza_model .get_application_config (app_name )
892+ port = app_config ['port' ].get ('value' )
893+ access_key = app_config ['root-user' ].get ('value' )
894+ access_secret = app_config ['root-password' ].get ('value' )
895+
896+ return boto3 .resource (
897+ "s3" ,
898+ verify = False ,
899+ endpoint_url = "http://{}:{}" .format (unit_address , port ),
900+ aws_access_key_id = access_key ,
901+ aws_secret_access_key = access_secret )
902+
859903 def configure_rgw_apps_for_multisite (self ):
860904 """Configure Multisite values on primary and secondary apps."""
861905 realm = 'zaza_realm'
@@ -877,6 +921,15 @@ def configure_rgw_apps_for_multisite(self):
877921 'zone' : 'zaza_secondary'
878922 }
879923 )
924+ if self .cloud_sync :
925+ zaza_model .set_application_config (
926+ self .cloud_sync_rgw_app ,
927+ {
928+ 'realm' : realm ,
929+ 'zonegroup' : zonegroup ,
930+ 'zone' : 'zaza_cloud_sync'
931+ }
932+ )
880933
881934 def clean_rgw_multisite_config (self , app_name ):
882935 """Clear Multisite Juju config values to default.
@@ -1048,6 +1101,13 @@ def test_003_object_storage_and_secondary_block(self):
10481101 "Non-Pristine RGW site can't be used as secondary"
10491102 }
10501103 }
1104+ if self .cloud_sync :
1105+ assert_state [self .cloud_sync_rgw_app ] = {
1106+ "workload-status" : "blocked" ,
1107+ "workload-status-message-prefix" :
1108+ "multi-site configuration but primary/secondary "
1109+ "relation missing" ,
1110+ }
10511111 zaza_model .wait_for_application_states (states = assert_state ,
10521112 timeout = 900 )
10531113
@@ -1066,6 +1126,99 @@ def test_003_object_storage_and_secondary_block(self):
10661126 zaza_model .block_until_unit_wl_status (self .secondary_rgw_unit ,
10671127 'active' )
10681128
1129+ def test_004_object_storage_cloud_sync (self ):
1130+ """Verify Ceph RGW Cloud Sync functionality."""
1131+ # Skip cloud sync tests if not compatible with bundle.
1132+ if not self .cloud_sync :
1133+ raise unittest .SkipTest ('Skipping Cloud Sync Test' )
1134+
1135+ obj_name = 'testfile'
1136+ # Syncs to default S3 target.
1137+ default_container_name = 'zaza-cloud-sync-container'
1138+ default_obj_data = 'Test data from Zaza'
1139+ # Syncs to dev S3 target.
1140+ dev_container_name = 'dev-zaza-cloud-sync-container'
1141+ dev_obj_data = 'Test dev data from Zaza'
1142+
1143+ # Configure cloud-sync multi-site relation.
1144+ logging .info ('Configuring Cloud Sync Multisite' )
1145+ self .configure_rgw_apps_for_multisite ()
1146+ zaza_model .add_relation (
1147+ self .primary_rgw_app ,
1148+ self .primary_rgw_app + ":primary" ,
1149+ self .cloud_sync_rgw_app + ":cloud-sync"
1150+ )
1151+ assert_state = {
1152+ self .secondary_rgw_app : {
1153+ "workload-status" : "blocked" ,
1154+ "workload-status-message-prefix" :
1155+ "multi-site configuration but primary/secondary "
1156+ "relation missing" ,
1157+ }
1158+ }
1159+ zaza_model .wait_for_application_states (states = assert_state ,
1160+ timeout = 900 )
1161+
1162+ logging .info ('Verifying Ceph RGW Cloud Sync functionality' )
1163+
1164+ # Fetch Primary Endpoint Details.
1165+ primary_endpoint = self .get_rgw_endpoint (self .primary_rgw_unit )
1166+ self .assertNotEqual (primary_endpoint , None )
1167+
1168+ # Create RGW client and perform IO to be synced to both S3 targets.
1169+ access_key , secret_key = self .get_client_keys ()
1170+ primary_client = boto3 .resource ("s3" ,
1171+ verify = False ,
1172+ endpoint_url = primary_endpoint ,
1173+ aws_access_key_id = access_key ,
1174+ aws_secret_access_key = secret_key )
1175+ default_container = primary_client .Bucket (default_container_name )
1176+ default_container .create ()
1177+ default_obj = primary_client .Object (default_container_name , obj_name )
1178+ default_obj .put (Body = default_obj_data )
1179+ dev_container = primary_client .Bucket (dev_container_name )
1180+ dev_container .create ()
1181+ dev_obj = primary_client .Object (dev_container_name , obj_name )
1182+ dev_obj .put (Body = dev_obj_data )
1183+
1184+ # Wait for sync to complete.
1185+ logging .info ('Waiting for Cloud Sync Data and Metadata to Synchronize' )
1186+ self .wait_for_status (self .cloud_sync_rgw_app , is_primary = False )
1187+
1188+ # Create clients for the cloud-sync S3 targets.
1189+ default_s3_client = self .get_minio_boto3_client (
1190+ self .cloud_sync_default_s3_app
1191+ )
1192+ self .assertNotEqual (default_s3_client , None )
1193+ dev_s3_client = self .get_minio_boto3_client (self .cloud_sync_dev_s3_app )
1194+ self .assertNotEqual (dev_s3_client , None )
1195+
1196+ # Verify that data was properly synced.
1197+ logging .info ('Verifying Synced Data on S3 Targets' )
1198+ test_data = self .fetch_rgw_object (default_s3_client ,
1199+ default_container_name ,
1200+ obj_name )
1201+ self .assertEqual (test_data , default_obj_data )
1202+ test_data = self .fetch_rgw_object (dev_s3_client ,
1203+ dev_container_name ,
1204+ obj_name )
1205+ self .assertEqual (test_data , dev_obj_data )
1206+
1207+ # Perform cleanup.
1208+ logging .info ('Performing Cleanup' )
1209+ self .purge_bucket (self .primary_rgw_app , default_container_name )
1210+ self .purge_bucket (self .primary_rgw_app , dev_container_name )
1211+
1212+ # Wait for sync to complete.
1213+ self .wait_for_status (self .cloud_sync_rgw_app , is_primary = False )
1214+
1215+ # Validate that synced data was removed from the S3 targets.
1216+ logging .info ('Verifying that data was deleted on the S3 targets' )
1217+ with self .assertRaises (botocore .exceptions .ClientError ):
1218+ default_s3_client .Object (default_container_name , obj_name ).get ()
1219+ with self .assertRaises (botocore .exceptions .ClientError ):
1220+ dev_s3_client .Object (dev_container_name , obj_name ).get ()
1221+
10691222 def test_100_migration_and_multisite_failover (self ):
10701223 """Perform multisite migration and verify failover."""
10711224 container_name = 'zaza-container'
0 commit comments