@@ -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 application is present."""
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.
@@ -855,6 +870,35 @@ def get_rgw_endpoint(self, unit_name: str):
855870 except KeyError :
856871 return "http://{}:80" .format (unit_address )
857872
873+ def get_minio_boto3_client (self , app_name : str ):
874+ """Get boto3 client for MinIO application.
875+
876+ :param app_name: MinIO Juju app name.
877+ :type app_name: str
878+ """
879+ leader_unit = zaza_model .get_lead_unit (app_name )
880+ unit_address = zaza_model .get_unit_public_address (
881+ leader_unit ,
882+ self .model_name
883+ )
884+
885+ logging .debug ("Minio Leader Unit: {}, Endpoint: {}" .format (
886+ leader_unit .entity_id , unit_address ))
887+ if unit_address is None :
888+ return None
889+
890+ app_config = zaza_model .get_application_config (app_name )
891+ port = app_config ['port' ].get ('value' )
892+ access_key = app_config ['root-user' ].get ('value' )
893+ access_secret = app_config ['root-password' ].get ('value' )
894+
895+ return boto3 .resource (
896+ "s3" ,
897+ verify = False ,
898+ endpoint_url = "http://{}:{}" .format (unit_address , port ),
899+ aws_access_key_id = access_key ,
900+ aws_secret_access_key = access_secret )
901+
858902 def configure_rgw_apps_for_multisite (self ):
859903 """Configure Multisite values on primary and secondary apps."""
860904 realm = 'zaza_realm'
@@ -876,6 +920,15 @@ def configure_rgw_apps_for_multisite(self):
876920 'zone' : 'zaza_secondary'
877921 }
878922 )
923+ if self .cloud_sync :
924+ zaza_model .set_application_config (
925+ self .cloud_sync_rgw_app ,
926+ {
927+ 'realm' : realm ,
928+ 'zonegroup' : zonegroup ,
929+ 'zone' : 'zaza_cloud_sync'
930+ }
931+ )
879932
880933 def configure_rgw_multisite_relation (self ):
881934 """Configure multi-site relation between primary and secondary apps."""
@@ -1074,6 +1127,13 @@ def test_003_object_storage_and_secondary_block(self):
10741127 "Non-Pristine RGW site can't be used as secondary"
10751128 }
10761129 }
1130+ if self .cloud_sync :
1131+ assert_state [self .cloud_sync_rgw_app ] = {
1132+ "workload-status" : "blocked" ,
1133+ "workload-status-message-prefix" :
1134+ "multi-site configuration but primary/secondary "
1135+ "relation missing" ,
1136+ }
10771137 zaza_model .wait_for_application_states (states = assert_state ,
10781138 timeout = 900 )
10791139
@@ -1294,6 +1354,99 @@ def test_004_multisite_directional_sync_policy(self):
12941354 )
12951355 zaza_model .wait_for_unit_idle (self .primary_rgw_unit )
12961356
1357+ def test_005_object_storage_cloud_sync (self ):
1358+ """Verify Ceph RGW Cloud Sync functionality."""
1359+ # Skip cloud sync tests if not compatible with bundle.
1360+ if not self .cloud_sync :
1361+ raise unittest .SkipTest ('Skipping Cloud Sync Test' )
1362+
1363+ obj_name = 'testfile'
1364+ # Syncs to default S3 target.
1365+ default_container_name = 'zaza-cloud-sync-container'
1366+ default_obj_data = 'Test data from Zaza'
1367+ # Syncs to dev S3 target.
1368+ dev_container_name = 'dev-zaza-cloud-sync-container'
1369+ dev_obj_data = 'Test dev data from Zaza'
1370+
1371+ # Configure cloud-sync multi-site relation.
1372+ logging .info ('Configuring Cloud Sync Multisite' )
1373+ self .configure_rgw_apps_for_multisite ()
1374+ zaza_model .add_relation (
1375+ self .primary_rgw_app ,
1376+ self .primary_rgw_app + ":primary" ,
1377+ self .cloud_sync_rgw_app + ":cloud-sync"
1378+ )
1379+ assert_state = {
1380+ self .secondary_rgw_app : {
1381+ "workload-status" : "blocked" ,
1382+ "workload-status-message-prefix" :
1383+ "multi-site configuration but primary/secondary "
1384+ "relation missing" ,
1385+ }
1386+ }
1387+ zaza_model .wait_for_application_states (states = assert_state ,
1388+ timeout = 900 )
1389+
1390+ logging .info ('Verifying Ceph RGW Cloud Sync functionality' )
1391+
1392+ # Fetch Primary Endpoint Details.
1393+ primary_endpoint = self .get_rgw_endpoint (self .primary_rgw_unit )
1394+ self .assertNotEqual (primary_endpoint , None )
1395+
1396+ # Create RGW client and perform IO to be synced to both S3 targets.
1397+ access_key , secret_key = self .get_client_keys ()
1398+ primary_client = boto3 .resource ("s3" ,
1399+ verify = False ,
1400+ endpoint_url = primary_endpoint ,
1401+ aws_access_key_id = access_key ,
1402+ aws_secret_access_key = secret_key )
1403+ default_container = primary_client .Bucket (default_container_name )
1404+ default_container .create ()
1405+ default_obj = primary_client .Object (default_container_name , obj_name )
1406+ default_obj .put (Body = default_obj_data )
1407+ dev_container = primary_client .Bucket (dev_container_name )
1408+ dev_container .create ()
1409+ dev_obj = primary_client .Object (dev_container_name , obj_name )
1410+ dev_obj .put (Body = dev_obj_data )
1411+
1412+ # Wait for sync to complete.
1413+ logging .info ('Waiting for Cloud Sync Data and Metadata to Synchronize' )
1414+ self .wait_for_status (self .cloud_sync_rgw_app , is_primary = False )
1415+
1416+ # Create clients for the cloud-sync S3 targets.
1417+ default_s3_client = self .get_minio_boto3_client (
1418+ self .cloud_sync_default_s3_app
1419+ )
1420+ self .assertNotEqual (default_s3_client , None )
1421+ dev_s3_client = self .get_minio_boto3_client (self .cloud_sync_dev_s3_app )
1422+ self .assertNotEqual (dev_s3_client , None )
1423+
1424+ # Verify that data was properly synced.
1425+ logging .info ('Verifying Synced Data on S3 Targets' )
1426+ test_data = self .fetch_rgw_object (default_s3_client ,
1427+ default_container_name ,
1428+ obj_name )
1429+ self .assertEqual (test_data , default_obj_data )
1430+ test_data = self .fetch_rgw_object (dev_s3_client ,
1431+ dev_container_name ,
1432+ obj_name )
1433+ self .assertEqual (test_data , dev_obj_data )
1434+
1435+ # Perform cleanup.
1436+ logging .info ('Performing Cleanup' )
1437+ self .purge_bucket (self .primary_rgw_app , default_container_name )
1438+ self .purge_bucket (self .primary_rgw_app , dev_container_name )
1439+
1440+ # Wait for sync to complete.
1441+ self .wait_for_status (self .cloud_sync_rgw_app , is_primary = False )
1442+
1443+ # Validate that synced data was removed from the S3 targets.
1444+ logging .info ('Verifying that data was deleted on the S3 targets' )
1445+ with self .assertRaises (botocore .exceptions .ClientError ):
1446+ default_s3_client .Object (default_container_name , obj_name ).get ()
1447+ with self .assertRaises (botocore .exceptions .ClientError ):
1448+ dev_s3_client .Object (dev_container_name , obj_name ).get ()
1449+
12971450 def test_100_migration_and_multisite_failover (self ):
12981451 """Perform multisite migration and verify failover."""
12991452 container_name = 'zaza-container'
0 commit comments