import json
import random
import string
import sys
import time
import logging
import errno
import dateutil.parser

from itertools import combinations
from itertools import zip_longest
from io import StringIO

import boto
import boto.s3.connection
from boto.s3.website import WebsiteConfiguration
from boto.s3.cors import CORSConfiguration

from nose.tools import eq_ as eq
from nose.tools import assert_not_equal, assert_equal
from nose.plugins.attrib import attr
from nose.plugins.skip import SkipTest

from .multisite import Zone, ZoneGroup, Credentials

from .conn import get_gateway_connection
from .tools import assert_raises

class Config:
    """ test configuration """
    def __init__(self, **kwargs):
        # by default, wait up to 5 minutes before giving up on a sync checkpoint
        self.checkpoint_retries = kwargs.get('checkpoint_retries', 60)
        self.checkpoint_delay = kwargs.get('checkpoint_delay', 5)
        # allow some time for realm reconfiguration after changing master zone
        self.reconfigure_delay = kwargs.get('reconfigure_delay', 5)
        self.tenant = kwargs.get('tenant', '')

# rgw multisite tests, written against the interfaces provided in rgw_multi.
# these tests must be initialized and run by another module that provides
# implementations of these interfaces by calling init_multi()
realm = None
user = None
config = None
def init_multi(_realm, _user, _config=None):
    global realm
    realm = _realm
    global user
    user = _user
    global config
    config = _config or Config()
    realm_meta_checkpoint(realm)

def get_user():
    return user.id if user is not None else ''

def get_tenant():
    return config.tenant if config is not None and config.tenant is not None else ''

def get_realm():
    return realm

log = logging.getLogger('rgw_multi.tests')

num_buckets = 0
run_prefix=''.join(random.choice(string.ascii_lowercase) for _ in range(6))

num_roles = 0

def get_zone_connection(zone, credentials):
    """ connect to the zone's first gateway """
    if isinstance(credentials, list):
        credentials = credentials[0]
    return get_gateway_connection(zone.gateways[0], credentials)

def mdlog_list(zone, period = None):
    cmd = ['mdlog', 'list']
    if period:
        cmd += ['--period', period]
    (mdlog_json, _) = zone.cluster.admin(cmd, read_only=True)
    return json.loads(mdlog_json)

def mdlog_autotrim(zone):
    zone.cluster.admin(['mdlog', 'autotrim'])

def datalog_list(zone, args = None):
    cmd = ['datalog', 'list'] + (args or [])
    (datalog_json, _) = zone.cluster.admin(cmd, read_only=True)
    return json.loads(datalog_json)

def datalog_status(zone):
    cmd = ['datalog', 'status']
    (datalog_json, _) = zone.cluster.admin(cmd, read_only=True)
    return json.loads(datalog_json)

def datalog_autotrim(zone):
    zone.cluster.admin(['datalog', 'autotrim'])

def bilog_list(zone, bucket, args = None):
    cmd = ['bilog', 'list', '--bucket', bucket] + (args or [])
    cmd += ['--tenant', config.tenant, '--uid', user.name] if config.tenant else []
    bilog, _ = zone.cluster.admin(cmd, read_only=True)
    return json.loads(bilog)

def bilog_autotrim(zone, args = None):
    zone.cluster.admin(['bilog', 'autotrim'] + (args or []))

def bucket_layout(zone, bucket, args = None):
    (bl_output,_) = zone.cluster.admin(['bucket', 'layout', '--bucket', bucket] + (args or []))
    return json.loads(bl_output)

def parse_meta_sync_status(meta_sync_status_json):
    log.debug('current meta sync status=%s', meta_sync_status_json)
    sync_status = json.loads(meta_sync_status_json)

    sync_info = sync_status['sync_status']['info']
    global_sync_status = sync_info['status']
    num_shards = sync_info['num_shards']
    period = sync_info['period']
    realm_epoch = sync_info['realm_epoch']

    sync_markers=sync_status['sync_status']['markers']
    log.debug('sync_markers=%s', sync_markers)
    assert(num_shards == len(sync_markers))

    markers={}
    for i in range(num_shards):
        # get marker, only if it's an incremental marker for the same realm epoch
        if realm_epoch > sync_markers[i]['val']['realm_epoch'] or sync_markers[i]['val']['state'] == 0:
            markers[i] = ''
        else:
            markers[i] = sync_markers[i]['val']['marker']

    return period, realm_epoch, num_shards, markers

def meta_sync_status(zone):
    for _ in range(config.checkpoint_retries):
        cmd = ['metadata', 'sync', 'status'] + zone.zone_args()
        meta_sync_status_json, retcode = zone.cluster.admin(cmd, check_retcode=False, read_only=True)
        if retcode == 0:
            return parse_meta_sync_status(meta_sync_status_json)
        assert(retcode == 2) # ENOENT
        time.sleep(config.checkpoint_delay)

    assert False, 'failed to read metadata sync status for zone=%s' % zone.name

def meta_master_log_status(master_zone):
    cmd = ['mdlog', 'status'] + master_zone.zone_args()
    mdlog_status_json, retcode = master_zone.cluster.admin(cmd, read_only=True)
    mdlog_status = json.loads(mdlog_status_json)

    markers = {i: s['marker'] for i, s in enumerate(mdlog_status)}
    log.debug('master meta markers=%s', markers)
    return markers

def compare_meta_status(zone, log_status, sync_status):
    if len(log_status) != len(sync_status):
        log.error('len(log_status)=%d, len(sync_status)=%d', len(log_status), len(sync_status))
        return False

    msg = ''
    for i, l, s in zip(log_status, log_status.values(), sync_status.values()):
        if l > s:
            if len(msg):
                msg += ', '
            msg += 'shard=' + str(i) + ' master=' + l + ' target=' + s

    if len(msg) > 0:
        log.warning('zone %s behind master: %s', zone.name, msg)
        return False

    return True

def zone_meta_checkpoint(zone, meta_master_zone = None, master_status = None):
    if not meta_master_zone:
        meta_master_zone = zone.realm().meta_master_zone()
    if not master_status:
        master_status = meta_master_log_status(meta_master_zone)

    current_realm_epoch = realm.current_period.data['realm_epoch']

    log.info('starting meta checkpoint for zone=%s', zone.name)

    for _ in range(config.checkpoint_retries):
        period, realm_epoch, num_shards, sync_status = meta_sync_status(zone)
        if realm_epoch < current_realm_epoch:
            log.warning('zone %s is syncing realm epoch=%d, behind current realm epoch=%d',
                        zone.name, realm_epoch, current_realm_epoch)
        else:
            log.debug('log_status=%s', master_status)
            log.debug('sync_status=%s', sync_status)
            if compare_meta_status(zone, master_status, sync_status):
                log.info('finish meta checkpoint for zone=%s', zone.name)
                return

        time.sleep(config.checkpoint_delay)
    assert False, 'failed meta checkpoint for zone=%s' % zone.name

def zonegroup_meta_checkpoint(zonegroup, meta_master_zone = None, master_status = None):
    if not meta_master_zone:
        meta_master_zone = zonegroup.realm().meta_master_zone()
    if not master_status:
        master_status = meta_master_log_status(meta_master_zone)

    for zone in zonegroup.zones:
        if zone == meta_master_zone:
            continue
        zone_meta_checkpoint(zone, meta_master_zone, master_status)

def realm_meta_checkpoint(realm):
    log.info('meta checkpoint')

    meta_master_zone = realm.meta_master_zone()
    master_status = meta_master_log_status(meta_master_zone)

    for zonegroup in realm.current_period.zonegroups:
        zonegroup_meta_checkpoint(zonegroup, meta_master_zone, master_status)

def parse_data_sync_status(data_sync_status_json):
    log.debug('current data sync status=%s', data_sync_status_json)
    sync_status = json.loads(data_sync_status_json)

    global_sync_status=sync_status['sync_status']['info']['status']
    num_shards=sync_status['sync_status']['info']['num_shards']

    sync_markers=sync_status['sync_status']['markers']
    log.debug('sync_markers=%s', sync_markers)
    assert(num_shards == len(sync_markers))

    markers={}
    for i in range(num_shards):
        markers[i] = sync_markers[i]['val']['marker']

    return (num_shards, markers)

def data_sync_status(target_zone, source_zone):
    if target_zone == source_zone:
        return None

    for _ in range(config.checkpoint_retries):
        cmd = ['data', 'sync', 'status'] + target_zone.zone_args()
        cmd += ['--source-zone', source_zone.name]
        data_sync_status_json, retcode = target_zone.cluster.admin(cmd, check_retcode=False, read_only=True)
        if retcode == 0:
            return parse_data_sync_status(data_sync_status_json)

        assert(retcode == 2) # ENOENT
        time.sleep(config.checkpoint_delay)

    assert False, 'failed to read data sync status for target_zone=%s source_zone=%s' % \
                  (target_zone.name, source_zone.name)

def bucket_sync_status(target_zone, source_zone, bucket_name):
    if target_zone == source_zone:
        return None

    cmd = ['bucket', 'sync', 'markers'] + target_zone.zone_args()
    cmd += ['--source-zone', source_zone.name]
    cmd += ['--bucket', bucket_name]
    cmd += ['--tenant', config.tenant, '--uid', user.name] if config.tenant else []
    while True:
        bucket_sync_status_json, retcode = target_zone.cluster.admin(cmd, check_retcode=False, read_only=True)
        if retcode == 0:
            break

        assert(retcode == 2) # ENOENT

    sync_status = json.loads(bucket_sync_status_json)

    markers={}
    for entry in sync_status:
        val = entry['val']
        pos = val['inc_marker']['position'].split('#')[-1] # get rid of shard id; e.g., 6#00000000002.132.3 -> 00000000002.132.3
        markers[entry['key']] = pos

    return markers

def data_source_log_status(source_zone):
    source_cluster = source_zone.cluster
    cmd = ['datalog', 'status'] + source_zone.zone_args()
    datalog_status_json, retcode = source_cluster.admin(cmd, read_only=True)
    datalog_status = json.loads(datalog_status_json)

    markers = {i: s['marker'] for i, s in enumerate(datalog_status)}
    log.debug('data markers for zone=%s markers=%s', source_zone.name, markers)
    return markers

def bucket_source_log_status(source_zone, bucket_name):
    cmd = ['bilog', 'status'] + source_zone.zone_args()
    cmd += ['--bucket', bucket_name]
    cmd += ['--tenant', config.tenant, '--uid', user.name] if config.tenant else []
    source_cluster = source_zone.cluster
    bilog_status_json, retcode = source_cluster.admin(cmd, read_only=True)
    bilog_status = json.loads(bilog_status_json)

    m={}
    markers={}
    try:
        m = bilog_status['markers']
    except:
        pass

    for s in m:
        key = s['key']
        val = s['val']
        markers[key] = val

    log.debug('bilog markers for zone=%s bucket=%s markers=%s', source_zone.name, bucket_name, markers)
    return markers

def compare_data_status(target_zone, source_zone, log_status, sync_status):
    if len(log_status) != len(sync_status):
        log.error('len(log_status)=%d len(sync_status)=%d', len(log_status), len(sync_status))
        return False

    msg =  ''
    for i, l, s in zip(log_status, log_status.values(), sync_status.values()):
        if l > s:
            if len(msg):
                msg += ', '
            msg += 'shard=' + str(i) + ' master=' + l + ' target=' + s

    if len(msg) > 0:
        log.warning('data of zone %s behind zone %s: %s', target_zone.name, source_zone.name, msg)
        return False

    return True

def compare_bucket_status(target_zone, source_zone, bucket_name, log_status, sync_status):
    if len(log_status) != len(sync_status):
        log.error('len(log_status)=%d len(sync_status)=%d', len(log_status), len(sync_status))
        return False

    msg =  ''
    for i, l, s in zip(log_status, log_status.values(), sync_status.values()):
        if l > s:
            if len(msg):
                msg += ', '
            msg += 'shard=' + str(i) + ' master=' + l + ' target=' + s

    if len(msg) > 0:
        log.warning('bucket %s zone %s behind zone %s: %s', bucket_name, target_zone.name, source_zone.name, msg)
        return False

    return True

def zone_data_checkpoint(target_zone, source_zone):
    if not target_zone.syncs_from(source_zone.name):
        return

    log_status = data_source_log_status(source_zone)
    log.info('starting data checkpoint for target_zone=%s source_zone=%s', target_zone.name, source_zone.name)

    for _ in range(config.checkpoint_retries):
        num_shards, sync_status = data_sync_status(target_zone, source_zone)

        log.debug('log_status=%s', log_status)
        log.debug('sync_status=%s', sync_status)

        if compare_data_status(target_zone, source_zone, log_status, sync_status):
            log.info('finished data checkpoint for target_zone=%s source_zone=%s',
                     target_zone.name, source_zone.name)
            return
        time.sleep(config.checkpoint_delay)

    assert False, 'failed data checkpoint for target_zone=%s source_zone=%s' % \
                  (target_zone.name, source_zone.name)

def zonegroup_data_checkpoint(zonegroup_conns):
    for source_conn in zonegroup_conns.rw_zones:
        for target_conn in zonegroup_conns.zones:
            if source_conn.zone == target_conn.zone:
                continue
            log.debug('data checkpoint: source=%s target=%s', source_conn.zone.name, target_conn.zone.name)
            zone_data_checkpoint(target_conn.zone, source_conn.zone)

def zone_bucket_checkpoint(target_zone, source_zone, bucket_name):
    if not target_zone.syncs_from(source_zone.name):
        return

    cmd = ['bucket', 'sync', 'checkpoint']
    cmd += ['--bucket', bucket_name, '--source-zone', source_zone.name]
    retry_delay_ms = config.checkpoint_delay * 1000
    timeout_sec = config.checkpoint_retries * config.checkpoint_delay
    cmd += ['--retry-delay-ms', str(retry_delay_ms), '--timeout-sec', str(timeout_sec)]
    cmd += target_zone.zone_args()
    target_zone.cluster.admin(cmd, debug_rgw=1)

def zonegroup_bucket_checkpoint(zonegroup_conns, bucket_name):
    for source_conn in zonegroup_conns.rw_zones:
        for target_conn in zonegroup_conns.zones:
            if source_conn.zone == target_conn.zone:
                continue
            log.debug('bucket checkpoint: source=%s target=%s bucket=%s', source_conn.zone.name, target_conn.zone.name, bucket_name)
            zone_bucket_checkpoint(target_conn.zone, source_conn.zone, bucket_name)
    for source_conn, target_conn in combinations(zonegroup_conns.zones, 2):
        if target_conn.zone.has_buckets():
            target_conn.check_bucket_eq(source_conn, bucket_name)

def set_master_zone(zone):
    zone.modify(zone.cluster, ['--master'])
    zonegroup = zone.zonegroup
    zonegroup.period.update(zone, commit=True)
    zonegroup.master_zone = zone
    log.info('Set master zone=%s, waiting %ds for reconfiguration..', zone.name, config.reconfigure_delay)
    time.sleep(config.reconfigure_delay)

def set_sync_from_all(zone, flag):
    s = 'true' if flag else 'false'
    zone.modify(zone.cluster, ['--sync-from-all={}'.format(s)])
    zonegroup = zone.zonegroup
    zonegroup.period.update(zone, commit=True)
    log.info('Set sync_from_all flag on zone %s to %s', zone.name, s)
    time.sleep(config.reconfigure_delay)

def set_redirect_zone(zone, redirect_zone):
    id_str = redirect_zone.id if redirect_zone else ''
    zone.modify(zone.cluster, ['--redirect-zone={}'.format(id_str)])
    zonegroup = zone.zonegroup
    zonegroup.period.update(zone, commit=True)
    log.info('Set redirect_zone zone %s to "%s"', zone.name, id_str)
    time.sleep(config.reconfigure_delay)

def enable_bucket_sync(zone, bucket_name):
    cmd = ['bucket', 'sync', 'enable', '--bucket', bucket_name] + zone.zone_args()
    zone.cluster.admin(cmd)

def disable_bucket_sync(zone, bucket_name):
    cmd = ['bucket', 'sync', 'disable', '--bucket', bucket_name] + zone.zone_args()
    zone.cluster.admin(cmd)

def check_buckets_sync_status_obj_not_exist(zone, buckets):
    for _ in range(config.checkpoint_retries):
        cmd = ['log', 'list'] + zone.zone_arg()
        log_list, ret = zone.cluster.admin(cmd, check_retcode=False, read_only=True)
        for bucket in buckets:
            if log_list.find(':'+bucket+":") >= 0:
                break
        else:
            return
        time.sleep(config.checkpoint_delay)
    assert False

def gen_bucket_name():
    global num_buckets

    num_buckets += 1
    return run_prefix + '-' + str(num_buckets)

def gen_role_name():
    global num_roles

    num_roles += 1
    return "roles" + '-' + run_prefix + '-' + str(num_roles)

class ZonegroupConns:
    def __init__(self, zonegroup):
        self.zonegroup = zonegroup
        self.zones = []
        self.ro_zones = []
        self.rw_zones = []
        self.master_zone = None

        for z in zonegroup.zones:
            zone_conn = z.get_conn(user.credentials)
            self.zones.append(zone_conn)
            if z.is_read_only():
                self.ro_zones.append(zone_conn)
            else:
                self.rw_zones.append(zone_conn)

            if z == zonegroup.master_zone:
                self.master_zone = zone_conn

def check_all_buckets_exist(zone_conn, buckets):
    if not zone_conn.zone.has_buckets():
        return True

    for b in buckets:
        try:
            zone_conn.get_bucket(b)
        except:
            log.critical('zone %s does not contain bucket %s', zone_conn.zone.name, b)
            return False

    return True

def check_all_buckets_dont_exist(zone_conn, buckets):
    if not zone_conn.zone.has_buckets():
        return True

    for b in buckets:
        try:
            zone_conn.get_bucket(b)
        except:
            continue

        log.critical('zone %s contains bucket %s', zone.zone, b)
        return False

    return True

def create_role_per_zone(zonegroup_conns, roles_per_zone = 1):
    roles = []
    zone_role = []
    for zone in zonegroup_conns.rw_zones:
        for i in range(roles_per_zone):
            role_name = gen_role_name()
            log.info('create role zone=%s name=%s', zone.name, role_name)
            policy_document = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"arn:aws:iam:::user/testuser\"]},\"Action\":[\"sts:AssumeRole\"]}]}"
            role = zone.create_role("", role_name, policy_document, "")
            roles.append(role_name)
            zone_role.append((zone, role))

    return roles, zone_role

def create_bucket_per_zone(zonegroup_conns, buckets_per_zone = 1):
    buckets = []
    zone_bucket = []
    for zone in zonegroup_conns.rw_zones:
        for i in range(buckets_per_zone):
            bucket_name = gen_bucket_name()
            log.info('create bucket zone=%s name=%s', zone.name, bucket_name)
            bucket = zone.create_bucket(bucket_name)
            buckets.append(bucket_name)
            zone_bucket.append((zone, bucket))

    return buckets, zone_bucket

def create_bucket_per_zone_in_realm():
    buckets = []
    zone_bucket = []
    for zonegroup in realm.current_period.zonegroups:
        zg_conn = ZonegroupConns(zonegroup)
        b, z = create_bucket_per_zone(zg_conn)
        buckets.extend(b)
        zone_bucket.extend(z)
    return buckets, zone_bucket

def test_bucket_create():
    zonegroup = realm.master_zonegroup()
    zonegroup_conns = ZonegroupConns(zonegroup)
    buckets, _ = create_bucket_per_zone(zonegroup_conns)
    zonegroup_meta_checkpoint(zonegroup)

    for zone in zonegroup_conns.zones:
        assert check_all_buckets_exist(zone, buckets)

def test_bucket_recreate():
    zonegroup = realm.master_zonegroup()
    zonegroup_conns = ZonegroupConns(zonegroup)
    buckets, _ = create_bucket_per_zone(zonegroup_conns)
    zonegroup_meta_checkpoint(zonegroup)


    for zone in zonegroup_conns.zones:
        assert check_all_buckets_exist(zone, buckets)

    # recreate buckets on all zones, make sure they weren't removed
    for zone in zonegroup_conns.rw_zones:
        for bucket_name in buckets:
            bucket = zone.create_bucket(bucket_name)

    for zone in zonegroup_conns.zones:
        assert check_all_buckets_exist(zone, buckets)

    zonegroup_meta_checkpoint(zonegroup)

    for zone in zonegroup_conns.zones:
        assert check_all_buckets_exist(zone, buckets)

def test_bucket_remove():
    zonegroup = realm.master_zonegroup()
    zonegroup_conns = ZonegroupConns(zonegroup)
    buckets, zone_bucket = create_bucket_per_zone(zonegroup_conns)
    zonegroup_meta_checkpoint(zonegroup)

    for zone in zonegroup_conns.zones:
        assert check_all_buckets_exist(zone, buckets)

    for zone, bucket_name in zone_bucket:
        zone.conn.delete_bucket(bucket_name)

    zonegroup_meta_checkpoint(zonegroup)

    for zone in zonegroup_conns.zones:
        assert check_all_buckets_dont_exist(zone, buckets)

def get_bucket(zone, bucket_name):
    return zone.conn.get_bucket(bucket_name)

def get_key(zone, bucket_name, obj_name):
    b = get_bucket(zone, bucket_name)
    return b.get_key(obj_name)

def new_key(zone, bucket_name, obj_name):
    b = get_bucket(zone, bucket_name)
    return b.new_key(obj_name)

def check_bucket_eq(zone_conn1, zone_conn2, bucket):
    if zone_conn2.zone.has_buckets():
        zone_conn2.check_bucket_eq(zone_conn1, bucket.name)

def check_role_eq(zone_conn1, zone_conn2, role):
    if zone_conn2.zone.has_roles():
        zone_conn2.check_role_eq(zone_conn1, role['create_role_response']['create_role_result']['role']['role_name'])

def test_object_sync():
    zonegroup = realm.master_zonegroup()
    zonegroup_conns = ZonegroupConns(zonegroup)
    buckets, zone_bucket = create_bucket_per_zone(zonegroup_conns)

    objnames = [ 'myobj', '_myobj', ':', '&' ]
    content = 'asdasd'

    # don't wait for meta sync just yet
    for zone, bucket_name in zone_bucket:
        for objname in objnames:
            k = new_key(zone, bucket_name, objname)
            k.set_contents_from_string(content)

    zonegroup_meta_checkpoint(zonegroup)

    for source_conn, bucket in zone_bucket:
        for target_conn in zonegroup_conns.zones:
            if source_conn.zone == target_conn.zone:
                continue

            zone_bucket_checkpoint(target_conn.zone, source_conn.zone, bucket.name)
            check_bucket_eq(source_conn, target_conn, bucket)

def test_object_delete():
    zonegroup = realm.master_zonegroup()
    zonegroup_conns = ZonegroupConns(zonegroup)
    buckets, zone_bucket = create_bucket_per_zone(zonegroup_conns)

    objname = 'myobj'
    content = 'asdasd'

    # don't wait for meta sync just yet
    for zone, bucket in zone_bucket:
        k = new_key(zone, bucket, objname)
        k.set_contents_from_string(content)

    zonegroup_meta_checkpoint(zonegroup)

    # check object exists
    for source_conn, bucket in zone_bucket:
        for target_conn in zonegroup_conns.zones:
            if source_conn.zone == target_conn.zone:
                continue

            zone_bucket_checkpoint(target_conn.zone, source_conn.zone, bucket.name)
            check_bucket_eq(source_conn, target_conn, bucket)

    # check object removal
    for source_conn, bucket in zone_bucket:
        k = get_key(source_conn, bucket, objname)
        k.delete()
        for target_conn in zonegroup_conns.zones:
            if source_conn.zone == target_conn.zone:
                continue

            zone_bucket_checkpoint(target_conn.zone, source_conn.zone, bucket.name)
            check_bucket_eq(source_conn, target_conn, bucket)

def get_latest_object_version(key):
    for k in key.bucket.list_versions(key.name):
        if k.is_latest:
            return k
    return None

def test_versioned_object_incremental_sync():
    zonegroup = realm.master_zonegroup()
    zonegroup_conns = ZonegroupConns(zonegroup)
    buckets, zone_bucket = create_bucket_per_zone(zonegroup_conns)

    # enable versioning
    for _, bucket in zone_bucket:
        bucket.configure_versioning(True)

    zonegroup_meta_checkpoint(zonegroup)

    # upload a dummy object to each bucket and wait for sync. this forces each
    # bucket to finish a full sync and switch to incremental
    for source_conn, bucket in zone_bucket:
        new_key(source_conn, bucket, 'dummy').set_contents_from_string('')
        for target_conn in zonegroup_conns.zones:
            if source_conn.zone == target_conn.zone:
                continue
            zone_bucket_checkpoint(target_conn.zone, source_conn.zone, bucket.name)

    for _, bucket in zone_bucket:
        # create and delete multiple versions of an object from each zone
        for zone_conn in zonegroup_conns.rw_zones:
            obj = 'obj-' + zone_conn.name
            k = new_key(zone_conn, bucket, obj)

            k.set_contents_from_string('version1')
            log.debug('version1 id=%s', k.version_id)
            # don't delete version1 - this tests that the initial version
            # doesn't get squashed into later versions

            # create and delete the following object versions to test that
            # the operations don't race with each other during sync
            k.set_contents_from_string('version2')
            log.debug('version2 id=%s', k.version_id)
            k.bucket.delete_key(obj, version_id=k.version_id)

            k.set_contents_from_string('version3')
            log.debug('version3 id=%s', k.version_id)
            k.bucket.delete_key(obj, version_id=k.version_id)

    for _, bucket in zone_bucket:
        zonegroup_bucket_checkpoint(zonegroup_conns, bucket.name)

    for _, bucket in zone_bucket:
        # overwrite the acls to test that metadata-only entries are applied
        for zone_conn in zonegroup_conns.rw_zones:
            obj = 'obj-' + zone_conn.name
            k = new_key(zone_conn, bucket.name, obj)
            v = get_latest_object_version(k)
            v.make_public()

    for _, bucket in zone_bucket:
        zonegroup_bucket_checkpoint(zonegroup_conns, bucket.name)

def test_concurrent_versioned_object_incremental_sync():
    zonegroup = realm.master_zonegroup()
    zonegroup_conns = ZonegroupConns(zonegroup)
    zone = zonegroup_conns.rw_zones[0]

    # create a versioned bucket
    bucket = zone.create_bucket(gen_bucket_name())
    log.debug('created bucket=%s', bucket.name)
    bucket.configure_versioning(True)

    zonegroup_meta_checkpoint(zonegroup)

    # upload a dummy object and wait for sync. this forces each zone to finish
    # a full sync and switch to incremental
    new_key(zone, bucket, 'dummy').set_contents_from_string('')
    zonegroup_bucket_checkpoint(zonegroup_conns, bucket.name)

    # create several concurrent versions on each zone and let them race to sync
    obj = 'obj'
    for i in range(10):
        for zone_conn in zonegroup_conns.rw_zones:
            k = new_key(zone_conn, bucket, obj)
            k.set_contents_from_string('version1')
            log.debug('zone=%s version=%s', zone_conn.zone.name, k.version_id)

    zonegroup_bucket_checkpoint(zonegroup_conns, bucket.name)
    zonegroup_data_checkpoint(zonegroup_conns)

def test_version_suspended_incremental_sync():
    zonegroup = realm.master_zonegroup()
    zonegroup_conns = ZonegroupConns(zonegroup)

    zone = zonegroup_conns.rw_zones[0]

    # create a non-versioned bucket
    bucket = zone.create_bucket(gen_bucket_name())
    log.debug('created bucket=%s', bucket.name)
    zonegroup_meta_checkpoint(zonegroup)

    # upload an initial object
    key1 = new_key(zone, bucket, 'obj')
    key1.set_contents_from_string('')
    log.debug('created initial version id=%s', key1.version_id)
    zonegroup_bucket_checkpoint(zonegroup_conns, bucket.name)

    # enable versioning
    bucket.configure_versioning(True)
    zonegroup_meta_checkpoint(zonegroup)

    # re-upload the object as a new version
    key2 = new_key(zone, bucket, 'obj')
    key2.set_contents_from_string('')
    log.debug('created new version id=%s', key2.version_id)
    zonegroup_bucket_checkpoint(zonegroup_conns, bucket.name)

    # suspend versioning
    bucket.configure_versioning(False)
    zonegroup_meta_checkpoint(zonegroup)

    # re-upload the object as a 'null' version
    key3 = new_key(zone, bucket, 'obj')
    key3.set_contents_from_string('')
    log.debug('created null version id=%s', key3.version_id)
    zonegroup_bucket_checkpoint(zonegroup_conns, bucket.name)

def test_delete_marker_full_sync():
    zonegroup = realm.master_zonegroup()
    zonegroup_conns = ZonegroupConns(zonegroup)
    buckets, zone_bucket = create_bucket_per_zone(zonegroup_conns)

    # enable versioning
    for _, bucket in zone_bucket:
        bucket.configure_versioning(True)
    zonegroup_meta_checkpoint(zonegroup)

    for zone, bucket in zone_bucket:
        # upload an initial object
        key1 = new_key(zone, bucket, 'obj')
        key1.set_contents_from_string('')

        # create a delete marker
        key2 = new_key(zone, bucket, 'obj')
        key2.delete()

    # wait for full sync
    for _, bucket in zone_bucket:
        zonegroup_bucket_checkpoint(zonegroup_conns, bucket.name)

def test_suspended_delete_marker_full_sync():
    zonegroup = realm.master_zonegroup()
    zonegroup_conns = ZonegroupConns(zonegroup)
    buckets, zone_bucket = create_bucket_per_zone(zonegroup_conns)

    # enable/suspend versioning
    for _, bucket in zone_bucket:
        bucket.configure_versioning(True)
        bucket.configure_versioning(False)
    zonegroup_meta_checkpoint(zonegroup)

    for zone, bucket in zone_bucket:
        # upload an initial object
        key1 = new_key(zone, bucket, 'obj')
        key1.set_contents_from_string('')

        # create a delete marker
        key2 = new_key(zone, bucket, 'obj')
        key2.delete()

    # wait for full sync
    for _, bucket in zone_bucket:
        zonegroup_bucket_checkpoint(zonegroup_conns, bucket.name)

def test_bucket_versioning():
    buckets, zone_bucket = create_bucket_per_zone_in_realm()
    for _, bucket in zone_bucket:
        bucket.configure_versioning(True)
        res = bucket.get_versioning_status()
        key = 'Versioning'
        assert(key in res and res[key] == 'Enabled')

def test_bucket_acl():
    buckets, zone_bucket = create_bucket_per_zone_in_realm()
    for _, bucket in zone_bucket:
        assert(len(bucket.get_acl().acl.grants) == 1) # single grant on owner
        bucket.set_acl('public-read')
        assert(len(bucket.get_acl().acl.grants) == 2) # new grant on AllUsers

def test_bucket_cors():
    buckets, zone_bucket = create_bucket_per_zone_in_realm()
    for _, bucket in zone_bucket:
        cors_cfg = CORSConfiguration()
        cors_cfg.add_rule(['DELETE'], 'https://www.example.com', allowed_header='*', max_age_seconds=3000)
        bucket.set_cors(cors_cfg)
        assert(bucket.get_cors().to_xml() == cors_cfg.to_xml())

def test_bucket_delete_notempty():
    zonegroup = realm.master_zonegroup()
    zonegroup_conns = ZonegroupConns(zonegroup)
    buckets, zone_bucket = create_bucket_per_zone(zonegroup_conns)
    zonegroup_meta_checkpoint(zonegroup)

    for zone_conn, bucket_name in zone_bucket:
        # upload an object to each bucket on its own zone
        conn = zone_conn.get_connection()
        bucket = conn.get_bucket(bucket_name)
        k = bucket.new_key('foo')
        k.set_contents_from_string('bar')
        # attempt to delete the bucket before this object can sync
        try:
            conn.delete_bucket(bucket_name)
        except boto.exception.S3ResponseError as e:
            assert(e.error_code == 'BucketNotEmpty')
            continue
        assert False # expected 409 BucketNotEmpty

    # assert that each bucket still exists on the master
    c1 = zonegroup_conns.master_zone.conn
    for _, bucket_name in zone_bucket:
        assert c1.get_bucket(bucket_name)

def test_multi_period_incremental_sync():
    zonegroup = realm.master_zonegroup()
    if len(zonegroup.zones) < 3:
        raise SkipTest("test_multi_period_incremental_sync skipped. Requires 3 or more zones in master zonegroup.")

    # periods to include in mdlog comparison
    mdlog_periods = [realm.current_period.id]

    # create a bucket in each zone
    zonegroup_conns = ZonegroupConns(zonegroup)
    buckets, zone_bucket = create_bucket_per_zone(zonegroup_conns)

    zonegroup_meta_checkpoint(zonegroup)

    z1, z2, z3 = zonegroup.zones[0:3]
    assert(z1 == zonegroup.master_zone)

    # kill zone 3 gateways to freeze sync status to incremental in first period
    z3.stop()

    # change master to zone 2 -> period 2
    set_master_zone(z2)
    mdlog_periods += [realm.current_period.id]

    for zone_conn, _ in zone_bucket:
        if zone_conn.zone == z3:
            continue
        bucket_name = gen_bucket_name()
        log.info('create bucket zone=%s name=%s', zone_conn.name, bucket_name)
        bucket = zone_conn.conn.create_bucket(bucket_name)
        buckets.append(bucket_name)

    # wait for zone 1 to sync
    zone_meta_checkpoint(z1)

    # change master back to zone 1 -> period 3
    set_master_zone(z1)
    mdlog_periods += [realm.current_period.id]

    for zone_conn, bucket_name in zone_bucket:
        if zone_conn.zone == z3:
            continue
        bucket_name = gen_bucket_name()
        log.info('create bucket zone=%s name=%s', zone_conn.name, bucket_name)
        zone_conn.conn.create_bucket(bucket_name)
        buckets.append(bucket_name)

    # restart zone 3 gateway and wait for sync
    z3.start()
    zonegroup_meta_checkpoint(zonegroup)

    # verify that we end up with the same objects
    for bucket_name in buckets:
        for source_conn, _ in zone_bucket:
            for target_conn in zonegroup_conns.zones:
                if source_conn.zone == target_conn.zone:
                    continue

                if target_conn.zone.has_buckets():
                    target_conn.check_bucket_eq(source_conn, bucket_name)

    # verify that mdlogs are not empty and match for each period
    for period in mdlog_periods:
        master_mdlog = mdlog_list(z1, period)
        assert len(master_mdlog) > 0
        for zone in zonegroup.zones:
            if zone == z1:
                continue
            mdlog = mdlog_list(zone, period)
            assert len(mdlog) == len(master_mdlog)

    # autotrim mdlogs for master zone
    mdlog_autotrim(z1)

    # autotrim mdlogs for peers
    for zone in zonegroup.zones:
        if zone == z1:
            continue
        mdlog_autotrim(zone)

    # verify that mdlogs are empty for each period
    for period in mdlog_periods:
        for zone in zonegroup.zones:
            mdlog = mdlog_list(zone, period)
            assert len(mdlog) == 0

def test_datalog_autotrim():
    zonegroup = realm.master_zonegroup()
    zonegroup_conns = ZonegroupConns(zonegroup)
    buckets, zone_bucket = create_bucket_per_zone(zonegroup_conns)

    # upload an object to each zone to generate a datalog entry
    for zone, bucket in zone_bucket:
        k = new_key(zone, bucket.name, 'key')
        k.set_contents_from_string('body')

    # wait for metadata and data sync to catch up
    zonegroup_meta_checkpoint(zonegroup)
    zonegroup_data_checkpoint(zonegroup_conns)

    # trim each datalog
    for zone, _ in zone_bucket:
        # read max markers for each shard
        status = datalog_status(zone.zone)

        datalog_autotrim(zone.zone)

        for shard_id, shard_status in enumerate(status):
            try:
                before_trim = dateutil.parser.isoparse(shard_status['last_update'])
            except: # empty timestamps look like "0.000000" and will fail here
                continue
            entries = datalog_list(zone.zone, ['--shard-id', str(shard_id), '--max-entries', '1'])
            if not len(entries):
                continue
            after_trim = dateutil.parser.isoparse(entries[0]['timestamp'])
            assert before_trim < after_trim, "any datalog entries must be newer than trim"

def test_multi_zone_redirect():
    zonegroup = realm.master_zonegroup()
    if len(zonegroup.rw_zones) < 2:
        raise SkipTest("test_multi_period_incremental_sync skipped. Requires 3 or more zones in master zonegroup.")

    zonegroup_conns = ZonegroupConns(zonegroup)
    (zc1, zc2) = zonegroup_conns.rw_zones[0:2]

    z1, z2 = (zc1.zone, zc2.zone)

    set_sync_from_all(z2, False)

    # create a bucket on the first zone
    bucket_name = gen_bucket_name()
    log.info('create bucket zone=%s name=%s', z1.name, bucket_name)
    bucket = zc1.conn.create_bucket(bucket_name)
    obj = 'testredirect'

    key = bucket.new_key(obj)
    data = 'A'*512
    key.set_contents_from_string(data)

    zonegroup_meta_checkpoint(zonegroup)

    # try to read object from second zone (should fail)
    bucket2 = get_bucket(zc2, bucket_name)
    assert_raises(boto.exception.S3ResponseError, bucket2.get_key, obj)

    set_redirect_zone(z2, z1)

    key2 = bucket2.get_key(obj)

    eq(data, key2.get_contents_as_string(encoding='ascii'))

    key = bucket.new_key(obj)

    for x in ['a', 'b', 'c', 'd']:
        data = x*512
        key.set_contents_from_string(data)
        eq(data, key2.get_contents_as_string(encoding='ascii'))

    # revert config changes
    set_sync_from_all(z2, True)
    set_redirect_zone(z2, None)

def test_zonegroup_remove():
    zonegroup = realm.master_zonegroup()
    zonegroup_conns = ZonegroupConns(zonegroup)
    if len(zonegroup.zones) < 2:
        raise SkipTest("test_zonegroup_remove skipped. Requires 2 or more zones in master zonegroup.")

    zonegroup_meta_checkpoint(zonegroup)
    z1, z2 = zonegroup.zones[0:2]
    c1, c2 = (z1.cluster, z2.cluster)

    # get admin credentials out of existing zone
    system_key = z1.data['system_key']
    admin_creds = Credentials(system_key['access_key'], system_key['secret_key'])

    # create a new zone in zonegroup on c2 and commit
    zone = Zone('remove', zonegroup, c2)
    zone.create(c2, admin_creds.credential_args())
    zonegroup.zones.append(zone)
    zonegroup.period.update(zone, commit=True)

    zonegroup.remove(c1, zone)

    # another 'zonegroup remove' should fail with ENOENT
    _, retcode = zonegroup.remove(c1, zone, check_retcode=False)
    assert(retcode == 2) # ENOENT

    # delete the new zone
    zone.delete(c2)

    # validate the resulting period
    zonegroup.period.update(z1, commit=True)


def test_zg_master_zone_delete():

    master_zg = realm.master_zonegroup()
    master_zone = master_zg.master_zone

    assert(len(master_zg.zones) >= 1)
    master_cluster = master_zg.zones[0].cluster

    rm_zg = ZoneGroup('remove_zg')
    rm_zg.create(master_cluster)

    rm_zone = Zone('remove', rm_zg, master_cluster)
    rm_zone.create(master_cluster)
    master_zg.period.update(master_zone, commit=True)


    rm_zone.delete(master_cluster)
    # Period update: This should now fail as the zone will be the master zone
    # in that zg
    _, retcode = master_zg.period.update(master_zone, check_retcode=False)
    assert(retcode == errno.EINVAL)

    # Proceed to delete the zonegroup as well, previous period now does not
    # contain a dangling master_zone, this must succeed
    rm_zg.delete(master_cluster)
    master_zg.period.update(master_zone, commit=True)

def test_set_bucket_website():
    buckets, zone_bucket = create_bucket_per_zone_in_realm()
    for _, bucket in zone_bucket:
        website_cfg = WebsiteConfiguration(suffix='index.html',error_key='error.html')
        try:
            bucket.set_website_configuration(website_cfg)
        except boto.exception.S3ResponseError as e:
            if e.error_code == 'MethodNotAllowed':
                raise SkipTest("test_set_bucket_website skipped. Requires rgw_enable_static_website = 1.")
        assert(bucket.get_website_configuration_with_xml()[1] == website_cfg.to_xml())

def test_set_bucket_policy():
    policy = '''{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": "*"
  }]
}'''
    buckets, zone_bucket = create_bucket_per_zone_in_realm()
    for _, bucket in zone_bucket:
        bucket.set_policy(policy)
        assert(bucket.get_policy().decode('ascii') == policy)

@attr('bucket_sync_disable')
def test_bucket_sync_disable():
    zonegroup = realm.master_zonegroup()
    zonegroup_conns = ZonegroupConns(zonegroup)
    buckets, zone_bucket = create_bucket_per_zone(zonegroup_conns)
    zonegroup_meta_checkpoint(zonegroup)

    for bucket_name in buckets:
        disable_bucket_sync(realm.meta_master_zone(), bucket_name)

    for zone in zonegroup.zones:
        check_buckets_sync_status_obj_not_exist(zone, buckets)

    zonegroup_data_checkpoint(zonegroup_conns)

@attr('bucket_sync_disable')
def test_bucket_sync_enable_right_after_disable():
    zonegroup = realm.master_zonegroup()
    zonegroup_conns = ZonegroupConns(zonegroup)
    buckets, zone_bucket = create_bucket_per_zone(zonegroup_conns)

    objnames = ['obj1', 'obj2', 'obj3', 'obj4']
    content = 'asdasd'

    for zone, bucket in zone_bucket:
        for objname in objnames:
            k = new_key(zone, bucket.name, objname)
            k.set_contents_from_string(content)

    zonegroup_meta_checkpoint(zonegroup)

    for bucket_name in buckets:
        zonegroup_bucket_checkpoint(zonegroup_conns, bucket_name)

    for bucket_name in buckets:
        disable_bucket_sync(realm.meta_master_zone(), bucket_name)
        enable_bucket_sync(realm.meta_master_zone(), bucket_name)

    objnames_2 = ['obj5', 'obj6', 'obj7', 'obj8']

    for zone, bucket in zone_bucket:
        for objname in objnames_2:
            k = new_key(zone, bucket.name, objname)
            k.set_contents_from_string(content)

    for bucket_name in buckets:
        zonegroup_bucket_checkpoint(zonegroup_conns, bucket_name)

    zonegroup_data_checkpoint(zonegroup_conns)

@attr('bucket_sync_disable')
def test_bucket_sync_disable_enable():
    zonegroup = realm.master_zonegroup()
    zonegroup_conns = ZonegroupConns(zonegroup)
    buckets, zone_bucket = create_bucket_per_zone(zonegroup_conns)

    objnames = [ 'obj1', 'obj2', 'obj3', 'obj4' ]
    content = 'asdasd'

    for zone, bucket in zone_bucket:
        for objname in objnames:
            k = new_key(zone, bucket.name, objname)
            k.set_contents_from_string(content)

    zonegroup_meta_checkpoint(zonegroup)

    for bucket_name in buckets:
        zonegroup_bucket_checkpoint(zonegroup_conns, bucket_name)

    for bucket_name in buckets:
        disable_bucket_sync(realm.meta_master_zone(), bucket_name)

    zonegroup_meta_checkpoint(zonegroup)

    objnames_2 = [ 'obj5', 'obj6', 'obj7', 'obj8' ]

    for zone, bucket in zone_bucket:
        for objname in objnames_2:
            k = new_key(zone, bucket.name, objname)
            k.set_contents_from_string(content)

    for bucket_name in buckets:
        enable_bucket_sync(realm.meta_master_zone(), bucket_name)

    for bucket_name in buckets:
        zonegroup_bucket_checkpoint(zonegroup_conns, bucket_name)

    zonegroup_data_checkpoint(zonegroup_conns)

def test_multipart_object_sync():
    zonegroup = realm.master_zonegroup()
    zonegroup_conns = ZonegroupConns(zonegroup)
    buckets, zone_bucket = create_bucket_per_zone(zonegroup_conns)

    _, bucket = zone_bucket[0]

    # initiate a multipart upload
    upload = bucket.initiate_multipart_upload('MULTIPART')
    mp = boto.s3.multipart.MultiPartUpload(bucket)
    mp.key_name = upload.key_name
    mp.id = upload.id
    part_size = 5 * 1024 * 1024 # 5M min part size
    mp.upload_part_from_file(StringIO('a' * part_size), 1)
    mp.upload_part_from_file(StringIO('b' * part_size), 2)
    mp.upload_part_from_file(StringIO('c' * part_size), 3)
    mp.upload_part_from_file(StringIO('d' * part_size), 4)
    mp.complete_upload()

    zonegroup_meta_checkpoint(zonegroup)
    zonegroup_bucket_checkpoint(zonegroup_conns, bucket.name)

def test_encrypted_object_sync():
    zonegroup = realm.master_zonegroup()
    zonegroup_conns = ZonegroupConns(zonegroup)

    if len(zonegroup.rw_zones) < 2:
        raise SkipTest("test_zonegroup_remove skipped. Requires 2 or more zones in master zonegroup.")

    (zone1, zone2) = zonegroup_conns.rw_zones[0:2]

    # create a bucket on the first zone
    bucket_name = gen_bucket_name()
    log.info('create bucket zone=%s name=%s', zone1.name, bucket_name)
    bucket = zone1.conn.create_bucket(bucket_name)

    # upload an object with sse-c encryption
    sse_c_headers = {
        'x-amz-server-side-encryption-customer-algorithm': 'AES256',
        'x-amz-server-side-encryption-customer-key': 'pO3upElrwuEXSoFwCfnZPdSsmt/xWeFa0N9KgDijwVs=',
        'x-amz-server-side-encryption-customer-key-md5': 'DWygnHRtgiJ77HCm+1rvHw=='
    }
    key = bucket.new_key('testobj-sse-c')
    data = 'A'*512
    key.set_contents_from_string(data, headers=sse_c_headers)

    # upload an object with sse-kms encryption
    sse_kms_headers = {
        'x-amz-server-side-encryption': 'aws:kms',
        # testkey-1 must be present in 'rgw crypt s3 kms encryption keys' (vstart.sh adds this)
        'x-amz-server-side-encryption-aws-kms-key-id': 'testkey-1',
    }
    key = bucket.new_key('testobj-sse-kms')
    key.set_contents_from_string(data, headers=sse_kms_headers)

    # wait for the bucket metadata and data to sync
    zonegroup_meta_checkpoint(zonegroup)
    zone_bucket_checkpoint(zone2.zone, zone1.zone, bucket_name)

    # read the encrypted objects from the second zone
    bucket2 = get_bucket(zone2, bucket_name)
    key = bucket2.get_key('testobj-sse-c', headers=sse_c_headers)
    eq(data, key.get_contents_as_string(headers=sse_c_headers, encoding='ascii'))

    key = bucket2.get_key('testobj-sse-kms')
    eq(data, key.get_contents_as_string(encoding='ascii'))

def test_bucket_index_log_trim():
    zonegroup = realm.master_zonegroup()
    zonegroup_conns = ZonegroupConns(zonegroup)

    zone = zonegroup_conns.rw_zones[0]

    # create a test bucket, upload some objects, and wait for sync
    def make_test_bucket():
        name = gen_bucket_name()
        log.info('create bucket zone=%s name=%s', zone.name, name)
        bucket = zone.conn.create_bucket(name)
        for objname in ('a', 'b', 'c', 'd'):
            k = new_key(zone, name, objname)
            k.set_contents_from_string('foo')
        zonegroup_meta_checkpoint(zonegroup)
        zonegroup_bucket_checkpoint(zonegroup_conns, name)
        return bucket

    # create a 'cold' bucket
    cold_bucket = make_test_bucket()

    # trim with max-buckets=0 to clear counters for cold bucket. this should
    # prevent it from being considered 'active' by the next autotrim
    bilog_autotrim(zone.zone, [
        '--rgw-sync-log-trim-max-buckets', '0',
    ])

    # create an 'active' bucket
    active_bucket = make_test_bucket()

    # trim with max-buckets=1 min-cold-buckets=0 to trim active bucket only
    bilog_autotrim(zone.zone, [
        '--rgw-sync-log-trim-max-buckets', '1',
        '--rgw-sync-log-trim-min-cold-buckets', '0',
    ])

    # verify active bucket has empty bilog
    active_bilog = bilog_list(zone.zone, active_bucket.name)
    assert(len(active_bilog) == 0)

    # verify cold bucket has nonempty bilog
    cold_bilog = bilog_list(zone.zone, cold_bucket.name)
    assert(len(cold_bilog) > 0)

    # trim with min-cold-buckets=999 to trim all buckets
    bilog_autotrim(zone.zone, [
        '--rgw-sync-log-trim-max-buckets', '999',
        '--rgw-sync-log-trim-min-cold-buckets', '999',
    ])

    # verify cold bucket has empty bilog
    cold_bilog = bilog_list(zone.zone, cold_bucket.name)
    assert(len(cold_bilog) == 0)

def test_bucket_reshard_index_log_trim():
    zonegroup = realm.master_zonegroup()
    zonegroup_conns = ZonegroupConns(zonegroup)

    zone = zonegroup_conns.rw_zones[0]

    # create a test bucket, upload some objects, and wait for sync
    def make_test_bucket():
        name = gen_bucket_name()
        log.info('create bucket zone=%s name=%s', zone.name, name)
        bucket = zone.conn.create_bucket(name)
        for objname in ('a', 'b', 'c', 'd'):
            k = new_key(zone, name, objname)
            k.set_contents_from_string('foo')
        zonegroup_meta_checkpoint(zonegroup)
        zonegroup_bucket_checkpoint(zonegroup_conns, name)
        return bucket

    # create a 'test' bucket
    test_bucket = make_test_bucket()

    # checking bucket layout before resharding
    json_obj_1 = bucket_layout(zone.zone, test_bucket.name)
    assert(len(json_obj_1['layout']['logs']) == 1)

    first_gen = json_obj_1['layout']['current_index']['gen']

    before_reshard_bilog = bilog_list(zone.zone, test_bucket.name, ['--gen', str(first_gen)])
    assert(len(before_reshard_bilog) == 4)

    # Resharding the bucket
    zone.zone.cluster.admin(['bucket', 'reshard',
        '--bucket', test_bucket.name,
        '--num-shards', '3',
        '--yes-i-really-mean-it'])

    # checking bucket layout after 1st resharding
    json_obj_2 = bucket_layout(zone.zone, test_bucket.name)
    assert(len(json_obj_2['layout']['logs']) == 2)

    second_gen = json_obj_2['layout']['current_index']['gen']

    after_reshard_bilog = bilog_list(zone.zone, test_bucket.name, ['--gen', str(second_gen)])
    assert(len(after_reshard_bilog) == 0)

    # upload more objects
    for objname in ('e', 'f', 'g', 'h'):
        k = new_key(zone, test_bucket.name, objname)
        k.set_contents_from_string('foo')
    zonegroup_bucket_checkpoint(zonegroup_conns, test_bucket.name)

    # Resharding the bucket again
    zone.zone.cluster.admin(['bucket', 'reshard',
        '--bucket', test_bucket.name,
        '--num-shards', '3',
        '--yes-i-really-mean-it'])

    # checking bucket layout after 2nd resharding
    json_obj_3 = bucket_layout(zone.zone, test_bucket.name)
    assert(len(json_obj_3['layout']['logs']) == 3)

    zonegroup_bucket_checkpoint(zonegroup_conns, test_bucket.name)

    bilog_autotrim(zone.zone)

    # checking bucket layout after 1st bilog autotrim
    json_obj_4 = bucket_layout(zone.zone, test_bucket.name)
    assert(len(json_obj_4['layout']['logs']) == 2)

    bilog_autotrim(zone.zone)

    # checking bucket layout after 2nd bilog autotrim
    json_obj_5 = bucket_layout(zone.zone, test_bucket.name)
    assert(len(json_obj_5['layout']['logs']) == 1)

    bilog_autotrim(zone.zone)

    # upload more objects
    for objname in ('i', 'j', 'k', 'l'):
        k = new_key(zone, test_bucket.name, objname)
        k.set_contents_from_string('foo')
    zonegroup_bucket_checkpoint(zonegroup_conns, test_bucket.name)

    # verify the bucket has non-empty bilog
    test_bilog = bilog_list(zone.zone, test_bucket.name)
    assert(len(test_bilog) > 0)

@attr('bucket_reshard')
def test_bucket_reshard_incremental():
    zonegroup = realm.master_zonegroup()
    zonegroup_conns = ZonegroupConns(zonegroup)
    zone = zonegroup_conns.rw_zones[0]

    # create a bucket
    bucket = zone.create_bucket(gen_bucket_name())
    log.debug('created bucket=%s', bucket.name)
    zonegroup_meta_checkpoint(zonegroup)

    # upload some objects
    for objname in ('a', 'b', 'c', 'd'):
        k = new_key(zone, bucket.name, objname)
        k.set_contents_from_string('foo')
    zonegroup_bucket_checkpoint(zonegroup_conns, bucket.name)

    # reshard in each zone
    for z in zonegroup_conns.rw_zones:
        z.zone.cluster.admin(['bucket', 'reshard',
            '--bucket', bucket.name,
            '--num-shards', '3',
            '--yes-i-really-mean-it'])

    # upload more objects
    for objname in ('e', 'f', 'g', 'h'):
        k = new_key(zone, bucket.name, objname)
        k.set_contents_from_string('foo')
    zonegroup_bucket_checkpoint(zonegroup_conns, bucket.name)

@attr('bucket_reshard')
def test_bucket_reshard_full():
    zonegroup = realm.master_zonegroup()
    zonegroup_conns = ZonegroupConns(zonegroup)
    zone = zonegroup_conns.rw_zones[0]

    # create a bucket
    bucket = zone.create_bucket(gen_bucket_name())
    log.debug('created bucket=%s', bucket.name)
    zonegroup_meta_checkpoint(zonegroup)

    # stop gateways in other zones so we can force the bucket to full sync
    for z in zonegroup_conns.rw_zones[1:]:
        z.zone.stop()

    # use try-finally to restart gateways even if something fails
    try:
        # upload some objects
        for objname in ('a', 'b', 'c', 'd'):
            k = new_key(zone, bucket.name, objname)
            k.set_contents_from_string('foo')

        # reshard on first zone
        zone.zone.cluster.admin(['bucket', 'reshard',
            '--bucket', bucket.name,
            '--num-shards', '3',
            '--yes-i-really-mean-it'])

        # upload more objects
        for objname in ('e', 'f', 'g', 'h'):
            k = new_key(zone, bucket.name, objname)
            k.set_contents_from_string('foo')
    finally:
        for z in zonegroup_conns.rw_zones[1:]:
            z.zone.start()

    zonegroup_bucket_checkpoint(zonegroup_conns, bucket.name)

def test_bucket_creation_time():
    zonegroup = realm.master_zonegroup()
    zonegroup_conns = ZonegroupConns(zonegroup)

    zonegroup_meta_checkpoint(zonegroup)

    zone_buckets = [zone.get_connection().get_all_buckets() for zone in zonegroup_conns.rw_zones]
    for z1, z2 in combinations(zone_buckets, 2):
        for a, b in zip(z1, z2):
            eq(a.name, b.name)
            eq(a.creation_date, b.creation_date)

def get_bucket_shard_objects(zone, num_shards):
    """
    Get one object for each shard of the bucket index log
    """
    cmd = ['bucket', 'shard', 'objects'] + zone.zone_args()
    cmd += ['--num-shards', str(num_shards)]
    shardobjs_json, ret = zone.cluster.admin(cmd, read_only=True)
    assert ret == 0
    shardobjs = json.loads(shardobjs_json)
    return shardobjs['objs']

def write_most_shards(zone, bucket_name, num_shards):
    """
    Write one object to most (but not all) bucket index shards.
    """
    objs = get_bucket_shard_objects(zone.zone, num_shards)
    random.shuffle(objs)
    del objs[-(len(objs)//10):]
    for obj in objs:
        k = new_key(zone, bucket_name, obj)
        k.set_contents_from_string('foo')

def reshard_bucket(zone, bucket_name, num_shards):
    """
    Reshard a bucket
    """
    cmd = ['bucket', 'reshard'] + zone.zone_args()
    cmd += ['--bucket', bucket_name]
    cmd += ['--num-shards', str(num_shards)]
    cmd += ['--yes-i-really-mean-it']
    zone.cluster.admin(cmd)

def get_obj_names(zone, bucket_name, maxobjs):
    """
    Get names of objects in a bucket.
    """
    cmd = ['bucket', 'list'] + zone.zone_args()
    cmd += ['--bucket', bucket_name]
    cmd += ['--max-entries', str(maxobjs)]
    objs_json, _ = zone.cluster.admin(cmd, read_only=True)
    objs = json.loads(objs_json)
    return [o['name'] for o in objs]

def bucket_keys_eq(zone1, zone2, bucket_name):
    """
    Ensure that two buckets have the same keys, but get the lists through
    radosgw-admin rather than S3 so it can be used when radosgw isn't running.
    Only works for buckets of 10,000 objects since the tests calling it don't
    need more, and the output from bucket list doesn't have an obvious marker
    with which to continue.
    """
    keys1 = get_obj_names(zone1, bucket_name, 10000)
    keys2 = get_obj_names(zone2, bucket_name, 10000)
    for key1, key2 in zip_longest(keys1, keys2):
        if key1 is None:
            log.critical('key=%s is missing from zone=%s', key1.name,
                         zone1.name)
            assert False
        if key2 is None:
            log.critical('key=%s is missing from zone=%s', key2.name,
                         zone2.name)
            assert False

@attr('bucket_reshard')
def test_bucket_sync_run_basic_incremental():
    """
    Create several generations of objects, then run bucket sync
    run to ensure they're all processed.
    """
    zonegroup = realm.master_zonegroup()
    zonegroup_conns = ZonegroupConns(zonegroup)
    primary = zonegroup_conns.rw_zones[0]

    # create a bucket write objects to it and wait for them to sync, ensuring
    # we are in incremental.
    bucket = primary.create_bucket(gen_bucket_name())
    log.debug('created bucket=%s', bucket.name)
    zonegroup_meta_checkpoint(zonegroup)
    write_most_shards(primary, bucket.name, 11)
    zonegroup_bucket_checkpoint(zonegroup_conns, bucket.name)

    try:
        # stop gateways in other zones so we can rely on bucket sync run
        for secondary in zonegroup_conns.rw_zones[1:]:
            secondary.zone.stop()

        # build up multiple generations each with some objects written to
        # them.
        generations = [17, 19, 23, 29, 31, 37]
        for num_shards in generations:
            reshard_bucket(primary.zone, bucket.name, num_shards)
            write_most_shards(primary, bucket.name, num_shards)

        # bucket sync run on every secondary
        for secondary in zonegroup_conns.rw_zones[1:]:
            cmd = ['bucket', 'sync', 'run'] + secondary.zone.zone_args()
            cmd += ['--bucket', bucket.name, '--source-zone', primary.name]
            secondary.zone.cluster.admin(cmd)

            bucket_keys_eq(primary.zone, secondary.zone, bucket.name)

    finally:
        # Restart so bucket_checkpoint can actually fetch things from the
        # secondaries. Put this in a finally block so they restart even on
        # error.
        for secondary in zonegroup_conns.rw_zones[1:]:
            secondary.zone.start()

    zonegroup_bucket_checkpoint(zonegroup_conns, bucket.name)

def trash_bucket(zone, bucket_name):
    """
    Remove objects through radosgw-admin, zapping bilog to prevent the deletes
    from replicating.
    """
    objs = get_obj_names(zone, bucket_name, 10000)
    # Delete the objects
    for obj in objs:
        cmd = ['object', 'rm'] + zone.zone_args()
        cmd += ['--bucket', bucket_name]
        cmd += ['--object', obj]
        zone.cluster.admin(cmd)

    # Zap the bilog
    cmd = ['bilog', 'trim'] + zone.zone_args()
    cmd += ['--bucket', bucket_name]
    zone.cluster.admin(cmd)

@attr('bucket_reshard')
def test_zap_init_bucket_sync_run():
    """
    Create several generations of objects, trash them, then run bucket sync init
    and bucket sync run.
    """
    zonegroup = realm.master_zonegroup()
    zonegroup_conns = ZonegroupConns(zonegroup)
    primary = zonegroup_conns.rw_zones[0]

    bucket = primary.create_bucket(gen_bucket_name())
    log.debug('created bucket=%s', bucket.name)
    zonegroup_meta_checkpoint(zonegroup)

    # Write zeroth generation
    for obj in range(1, 6):
        k = new_key(primary, bucket.name, f'obj{obj * 11}')
        k.set_contents_from_string('foo')
    zonegroup_bucket_checkpoint(zonegroup_conns, bucket.name)

    # Write several more generations
    generations = [17, 19, 23, 29, 31, 37]
    for num_shards in generations:
        reshard_bucket(primary.zone, bucket.name, num_shards)
        for obj in range(1, 6):
            k = new_key(primary, bucket.name, f'obj{obj * num_shards}')
            k.set_contents_from_string('foo')
        zonegroup_bucket_checkpoint(zonegroup_conns, bucket.name)


    # Stop gateways, trash bucket, init, sync, and restart for every secondary
    for secondary in zonegroup_conns.rw_zones[1:]:
        try:
            secondary.zone.stop()

            trash_bucket(secondary.zone, bucket.name)

            cmd = ['bucket', 'sync', 'init'] + secondary.zone.zone_args()
            cmd += ['--bucket', bucket.name]
            cmd += ['--source-zone', primary.name]
            secondary.zone.cluster.admin(cmd)

            cmd = ['bucket', 'sync', 'run'] + secondary.zone.zone_args()
            cmd += ['--bucket', bucket.name, '--source-zone', primary.name]
            secondary.zone.cluster.admin(cmd)

            bucket_keys_eq(primary.zone, secondary.zone, bucket.name)

        finally:
            # Do this as a finally so we bring the zone back up even on error.
            secondary.zone.start()

    zonegroup_bucket_checkpoint(zonegroup_conns, bucket.name)

def test_role_sync():
    zonegroup = realm.master_zonegroup()
    zonegroup_conns = ZonegroupConns(zonegroup)
    roles, zone_role = create_role_per_zone(zonegroup_conns)

    zonegroup_meta_checkpoint(zonegroup)

    for source_conn, role in zone_role:
        for target_conn in zonegroup_conns.zones:
            if source_conn.zone == target_conn.zone:
                continue

            check_role_eq(source_conn, target_conn, role)

@attr('data_sync_init')
def test_bucket_full_sync_after_data_sync_init():
    zonegroup = realm.master_zonegroup()
    zonegroup_conns = ZonegroupConns(zonegroup)
    primary = zonegroup_conns.rw_zones[0]
    secondary = zonegroup_conns.rw_zones[1]

    bucket = primary.create_bucket(gen_bucket_name())
    log.debug('created bucket=%s', bucket.name)
    zonegroup_meta_checkpoint(zonegroup)

    try:
        # stop secondary zone before it starts a bucket full sync
        secondary.zone.stop()

        # write some objects that don't sync yet
        for obj in range(1, 6):
            k = new_key(primary, bucket.name, f'obj{obj * 11}')
            k.set_contents_from_string('foo')

        cmd = ['data', 'sync', 'init'] + secondary.zone.zone_args()
        cmd += ['--source-zone', primary.name]
        secondary.zone.cluster.admin(cmd)
    finally:
        # Do this as a finally so we bring the zone back up even on error.
        secondary.zone.start()

    # expect all objects to replicate via 'bucket full sync'
    zonegroup_bucket_checkpoint(zonegroup_conns, bucket.name)
    zonegroup_data_checkpoint(zonegroup_conns)

@attr('data_sync_init')
@attr('bucket_reshard')
def test_resharded_bucket_full_sync_after_data_sync_init():
    zonegroup = realm.master_zonegroup()
    zonegroup_conns = ZonegroupConns(zonegroup)
    primary = zonegroup_conns.rw_zones[0]
    secondary = zonegroup_conns.rw_zones[1]

    bucket = primary.create_bucket(gen_bucket_name())
    log.debug('created bucket=%s', bucket.name)
    zonegroup_meta_checkpoint(zonegroup)

    try:
        # stop secondary zone before it starts a bucket full sync
        secondary.zone.stop()

        # Write zeroth generation
        for obj in range(1, 6):
            k = new_key(primary, bucket.name, f'obj{obj * 11}')
            k.set_contents_from_string('foo')

        # Write several more generations
        generations = [17, 19, 23, 29, 31, 37]
        for num_shards in generations:
            reshard_bucket(primary.zone, bucket.name, num_shards)
            for obj in range(1, 6):
                k = new_key(primary, bucket.name, f'obj{obj * num_shards}')
                k.set_contents_from_string('foo')

        cmd = ['data', 'sync', 'init'] + secondary.zone.zone_args()
        cmd += ['--source-zone', primary.name]
        secondary.zone.cluster.admin(cmd)
    finally:
        # Do this as a finally so we bring the zone back up even on error.
        secondary.zone.start()

    # expect all objects to replicate via 'bucket full sync'
    zonegroup_bucket_checkpoint(zonegroup_conns, bucket.name)
    zonegroup_data_checkpoint(zonegroup_conns)

@attr('data_sync_init')
def test_bucket_incremental_sync_after_data_sync_init():
    zonegroup = realm.master_zonegroup()
    zonegroup_conns = ZonegroupConns(zonegroup)
    primary = zonegroup_conns.rw_zones[0]
    secondary = zonegroup_conns.rw_zones[1]

    bucket = primary.create_bucket(gen_bucket_name())
    log.debug('created bucket=%s', bucket.name)
    zonegroup_meta_checkpoint(zonegroup)

    # upload a dummy object and wait for sync. this forces each zone to finish
    # a full sync and switch to incremental
    k = new_key(primary, bucket, 'dummy')
    k.set_contents_from_string('foo')
    zonegroup_bucket_checkpoint(zonegroup_conns, bucket.name)

    try:
        # stop secondary zone before it syncs the rest
        secondary.zone.stop()

        # Write more objects to primary
        for obj in range(1, 6):
            k = new_key(primary, bucket.name, f'obj{obj * 11}')
            k.set_contents_from_string('foo')

        cmd = ['data', 'sync', 'init'] + secondary.zone.zone_args()
        cmd += ['--source-zone', primary.name]
        secondary.zone.cluster.admin(cmd)
    finally:
        # Do this as a finally so we bring the zone back up even on error.
        secondary.zone.start()

    # expect remaining objects to replicate via 'bucket incremental sync'
    zonegroup_bucket_checkpoint(zonegroup_conns, bucket.name)
    zonegroup_data_checkpoint(zonegroup_conns)

@attr('data_sync_init')
@attr('bucket_reshard')
def test_resharded_bucket_incremental_sync_latest_after_data_sync_init():
    zonegroup = realm.master_zonegroup()
    zonegroup_conns = ZonegroupConns(zonegroup)
    primary = zonegroup_conns.rw_zones[0]
    secondary = zonegroup_conns.rw_zones[1]

    bucket = primary.create_bucket(gen_bucket_name())
    log.debug('created bucket=%s', bucket.name)
    zonegroup_meta_checkpoint(zonegroup)

    # Write zeroth generation to primary
    for obj in range(1, 6):
        k = new_key(primary, bucket.name, f'obj{obj * 11}')
        k.set_contents_from_string('foo')

    # Write several more generations
    generations = [17, 19, 23, 29, 31, 37]
    for num_shards in generations:
        reshard_bucket(primary.zone, bucket.name, num_shards)
        for obj in range(1, 6):
            k = new_key(primary, bucket.name, f'obj{obj * num_shards}')
            k.set_contents_from_string('foo')

    # wait for the secondary to catch up to the latest gen
    zonegroup_bucket_checkpoint(zonegroup_conns, bucket.name)

    try:
        # stop secondary zone before it syncs the rest
        secondary.zone.stop()

        # write some more objects to the last gen
        for obj in range(1, 6):
            k = new_key(primary, bucket.name, f'obj{obj * generations[-1]}')
            k.set_contents_from_string('foo')

        cmd = ['data', 'sync', 'init'] + secondary.zone.zone_args()
        cmd += ['--source-zone', primary.name]
        secondary.zone.cluster.admin(cmd)
    finally:
        # Do this as a finally so we bring the zone back up even on error.
        secondary.zone.start()

    # expect remaining objects in last gen to replicate via 'bucket incremental sync'
    zonegroup_bucket_checkpoint(zonegroup_conns, bucket.name)
    zonegroup_data_checkpoint(zonegroup_conns)

@attr('data_sync_init')
@attr('bucket_reshard')
def test_resharded_bucket_incremental_sync_oldest_after_data_sync_init():
    zonegroup = realm.master_zonegroup()
    zonegroup_conns = ZonegroupConns(zonegroup)
    primary = zonegroup_conns.rw_zones[0]
    secondary = zonegroup_conns.rw_zones[1]

    bucket = primary.create_bucket(gen_bucket_name())
    log.debug('created bucket=%s', bucket.name)
    zonegroup_meta_checkpoint(zonegroup)

    # Write zeroth generation to primary
    for obj in range(1, 6):
        k = new_key(primary, bucket.name, f'obj{obj * 11}')
        k.set_contents_from_string('foo')

    # wait for the secondary to catch up
    zonegroup_bucket_checkpoint(zonegroup_conns, bucket.name)

    try:
        # stop secondary zone before it syncs later generations
        secondary.zone.stop()

        # Write several more generations
        generations = [17, 19, 23, 29, 31, 37]
        for num_shards in generations:
            reshard_bucket(primary.zone, bucket.name, num_shards)
            for obj in range(1, 6):
                k = new_key(primary, bucket.name, f'obj{obj * num_shards}')
                k.set_contents_from_string('foo')

        cmd = ['data', 'sync', 'init'] + secondary.zone.zone_args()
        cmd += ['--source-zone', primary.name]
        secondary.zone.cluster.admin(cmd)
    finally:
        # Do this as a finally so we bring the zone back up even on error.
        secondary.zone.start()

    # expect all generations to replicate via 'bucket incremental sync'
    zonegroup_bucket_checkpoint(zonegroup_conns, bucket.name)
    zonegroup_data_checkpoint(zonegroup_conns)

def sync_info(cluster, bucket = None):
    cmd = ['sync', 'info']
    if bucket:
        cmd += ['--bucket', bucket]
    (result_json, retcode) = cluster.admin(cmd)
    if retcode != 0:
        assert False, 'failed to get sync policy'

    return json.loads(result_json)

def get_sync_policy(cluster, bucket = None):
    cmd = ['sync', 'policy', 'get']
    if bucket:
        cmd += ['--bucket', bucket]
    (result_json, retcode) = cluster.admin(cmd)
    if retcode != 0:
        assert False, 'failed to get sync policy'

    return json.loads(result_json)

def create_sync_policy_group(cluster, group, status = "allowed", bucket = None):
    cmd = ['sync', 'group', 'create', '--group-id', group, '--status' , status]
    if bucket:
        cmd += ['--bucket', bucket]
    (result_json, retcode) = cluster.admin(cmd)
    if retcode != 0:
        assert False, 'failed to create sync policy group id=%s, bucket=%s' % (group, bucket)
    return json.loads(result_json)

def set_sync_policy_group_status(cluster, group, status, bucket = None):
    cmd = ['sync', 'group', 'modify', '--group-id', group, '--status' , status]
    if bucket:
        cmd += ['--bucket', bucket]
    (result_json, retcode) = cluster.admin(cmd)
    if retcode != 0:
        assert False, 'failed to set sync policy group id=%s, bucket=%s' % (group, bucket)
    return json.loads(result_json)

def get_sync_policy_group(cluster, group, bucket = None):
    cmd = ['sync', 'group', 'get', '--group-id', group]
    if bucket:
        cmd += ['--bucket', bucket]
    (result_json, retcode) = cluster.admin(cmd)
    if retcode != 0:
        assert False, 'failed to get sync policy group id=%s, bucket=%s' % (group, bucket)
    return json.loads(result_json)

def remove_sync_policy_group(cluster, group, bucket = None):
    cmd = ['sync', 'group', 'remove', '--group-id', group]
    if bucket:
        cmd += ['--bucket', bucket]
    (result_json, retcode) = cluster.admin(cmd)
    if retcode != 0:
        assert False, 'failed to remove sync policy group id=%s, bucket=%s' % (group, bucket)
    return json.loads(result_json)

def create_sync_group_flow_symmetrical(cluster, group, flow_id, zones, bucket = None):
    cmd = ['sync', 'group', 'flow', 'create', '--group-id', group, '--flow-id' , flow_id, '--flow-type', 'symmetrical', '--zones=%s' % zones]
    if bucket:
        cmd += ['--bucket', bucket]
    (result_json, retcode) = cluster.admin(cmd)
    if retcode != 0:
        assert False, 'failed to create sync group flow symmetrical groupid=%s, flow_id=%s, zones=%s, bucket=%s' % (group, flow_id, zones, bucket)
    return json.loads(result_json)

def create_sync_group_flow_directional(cluster, group, flow_id, src_zones, dest_zones, bucket = None):
    cmd = ['sync', 'group', 'flow', 'create', '--group-id', group, '--flow-id' , flow_id, '--flow-type', 'directional', '--source-zone=%s' % src_zones, '--dest-zone=%s' % dest_zones]
    if bucket:
        cmd += ['--bucket', bucket]
    (result_json, retcode) = cluster.admin(cmd)
    if retcode != 0:
        assert False, 'failed to create sync group flow directional groupid=%s, flow_id=%s, src_zones=%s, dest_zones=%s, bucket=%s' % (group, flow_id, src_zones, dest_zones, bucket)
    return json.loads(result_json)

def remove_sync_group_flow_symmetrical(cluster, group, flow_id, zones = None, bucket = None):
    cmd = ['sync', 'group', 'flow', 'remove', '--group-id', group, '--flow-id' , flow_id, '--flow-type', 'symmetrical']
    if zones:
        cmd += ['--zones=%s' % zones]
    if bucket:
        cmd += ['--bucket', bucket]
    (result_json, retcode) = cluster.admin(cmd)
    if retcode != 0:
        assert False, 'failed to remove sync group flow symmetrical groupid=%s, flow_id=%s, zones=%s, bucket=%s' % (group, flow_id, zones, bucket)
    return json.loads(result_json)

def remove_sync_group_flow_directional(cluster, group, flow_id, src_zones, dest_zones, bucket = None):
    cmd = ['sync', 'group', 'flow', 'remove', '--group-id', group, '--flow-id' , flow_id, '--flow-type', 'directional', '--source-zone=%s' % src_zones, '--dest-zone=%s' % dest_zones]
    if bucket:
        cmd += ['--bucket', bucket]
    (result_json, retcode) = cluster.admin(cmd)
    if retcode != 0:
        assert False, 'failed to remove sync group flow directional groupid=%s, flow_id=%s, src_zones=%s, dest_zones=%s, bucket=%s' % (group, flow_id, src_zones, dest_zones, bucket)
    return json.loads(result_json)

def create_sync_group_pipe(cluster, group, pipe_id, src_zones, dest_zones, bucket = None, args = []):
    cmd = ['sync', 'group', 'pipe', 'create', '--group-id', group, '--pipe-id' , pipe_id, '--source-zones=%s' % src_zones, '--dest-zones=%s' % dest_zones]
    if bucket:
        b_args = '--bucket=' + bucket
        cmd.append(b_args)
    if args:
        cmd += args
    (result_json, retcode) = cluster.admin(cmd)
    if retcode != 0:
        assert False, 'failed to create sync group pipe groupid=%s, pipe_id=%s, src_zones=%s, dest_zones=%s, bucket=%s' % (group, pipe_id, src_zones, dest_zones, bucket)
    return json.loads(result_json)

def remove_sync_group_pipe(cluster, group, pipe_id, bucket = None, args = None):
    cmd = ['sync', 'group', 'pipe', 'remove', '--group-id', group, '--pipe-id' , pipe_id]
    if bucket:
        b_args = '--bucket=' + bucket
        cmd.append(b_args)
    if args:
        cmd.append(args)
    (result_json, retcode) = cluster.admin(cmd)
    if retcode != 0:
        assert False, 'failed to remove sync group pipe groupid=%s, pipe_id=%s, src_zones=%s, dest_zones=%s, bucket=%s' % (group, pipe_id, src_zones, dest_zones, bucket)
    return json.loads(result_json)

def create_zone_bucket(zone):
    b_name = gen_bucket_name()
    log.info('create bucket zone=%s name=%s', zone.name, b_name)
    bucket = zone.create_bucket(b_name)
    return bucket

def create_object(zone_conn, bucket, objname, content):
    k = new_key(zone_conn, bucket.name, objname)
    k.set_contents_from_string(content)

def create_objects(zone_conn, bucket, obj_arr, content):
    for objname in obj_arr:
        create_object(zone_conn, bucket, objname, content)

def check_object_exists(bucket, objname, content = None):
    k = bucket.get_key(objname)
    assert_not_equal(k, None)
    if (content != None):
        assert_equal(k.get_contents_as_string(encoding='ascii'), content)

def check_objects_exist(bucket, obj_arr, content = None):
    for objname in obj_arr:
        check_object_exists(bucket, objname, content)

def check_object_not_exists(bucket, objname):
    k = bucket.get_key(objname)
    assert_equal(k, None)

def check_objects_not_exist(bucket, obj_arr):
    for objname in obj_arr:
        check_object_not_exists(bucket, objname)

@attr('sync_policy')
def test_sync_policy_config_zonegroup():
    """
    test_sync_policy_config_zonegroup:
        test configuration of all sync commands
    """
    zonegroup = realm.master_zonegroup()
    zonegroup_meta_checkpoint(zonegroup)

    zonegroup_conns = ZonegroupConns(zonegroup)
    z1, z2 = zonegroup.zones[0:2]
    c1, c2 = (z1.cluster, z2.cluster)

    zones = z1.name+","+z2.name

    c1.admin(['sync', 'policy', 'get'])

    # (a) zonegroup level
    create_sync_policy_group(c1, "sync-group")
    set_sync_policy_group_status(c1, "sync-group", "enabled")
    get_sync_policy_group(c1, "sync-group")

    get_sync_policy(c1)

    create_sync_group_flow_symmetrical(c1, "sync-group", "sync-flow1", zones)
    create_sync_group_flow_directional(c1, "sync-group", "sync-flow2", z1.name, z2.name)

    create_sync_group_pipe(c1, "sync-group", "sync-pipe", zones, zones)
    get_sync_policy_group(c1, "sync-group")

    zonegroup.period.update(z1, commit=True)

    # (b)  bucket level
    zc1, zc2 = zonegroup_conns.zones[0:2]
    bucket = create_zone_bucket(zc1)
    bucket_name = bucket.name

    create_sync_policy_group(c1, "sync-bucket", "allowed", bucket_name)
    set_sync_policy_group_status(c1, "sync-bucket", "enabled", bucket_name)
    get_sync_policy_group(c1, "sync-bucket", bucket_name)

    get_sync_policy(c1, bucket_name)

    create_sync_group_flow_symmetrical(c1, "sync-bucket", "sync-flow1", zones, bucket_name)
    create_sync_group_flow_directional(c1, "sync-bucket", "sync-flow2", z1.name, z2.name, bucket_name)

    create_sync_group_pipe(c1, "sync-bucket", "sync-pipe", zones, zones, bucket_name)
    get_sync_policy_group(c1, "sync-bucket", bucket_name)

    zonegroup_meta_checkpoint(zonegroup)

    remove_sync_group_pipe(c1, "sync-bucket", "sync-pipe", bucket_name)
    remove_sync_group_flow_directional(c1, "sync-bucket", "sync-flow2", z1.name, z2.name, bucket_name)
    remove_sync_group_flow_symmetrical(c1, "sync-bucket", "sync-flow1", zones, bucket_name)
    remove_sync_policy_group(c1, "sync-bucket", bucket_name)

    get_sync_policy(c1, bucket_name)

    zonegroup_meta_checkpoint(zonegroup)

    remove_sync_group_pipe(c1, "sync-group", "sync-pipe")
    remove_sync_group_flow_directional(c1, "sync-group", "sync-flow2", z1.name, z2.name)
    remove_sync_group_flow_symmetrical(c1, "sync-group", "sync-flow1")
    remove_sync_policy_group(c1, "sync-group")

    get_sync_policy(c1)

    zonegroup.period.update(z1, commit=True)

    return

@attr('sync_policy')
def test_sync_flow_symmetrical_zonegroup_all():
    """
    test_sync_flow_symmetrical_zonegroup_all:
        allows sync from all the zones to all other zones (default case)
    """

    zonegroup = realm.master_zonegroup()
    zonegroup_meta_checkpoint(zonegroup)

    zonegroup_conns = ZonegroupConns(zonegroup)

    (zoneA, zoneB) = zonegroup.zones[0:2]
    (zcA, zcB) = zonegroup_conns.zones[0:2]

    c1 = zoneA.cluster

    c1.admin(['sync', 'policy', 'get'])

    zones = zoneA.name + ',' + zoneB.name
    create_sync_policy_group(c1, "sync-group")
    create_sync_group_flow_symmetrical(c1, "sync-group", "sync-flow1", zones)
    create_sync_group_pipe(c1, "sync-group", "sync-pipe", zones, zones)
    set_sync_policy_group_status(c1, "sync-group", "enabled")

    zonegroup.period.update(zoneA, commit=True)
    get_sync_policy(c1)

    objnames = [ 'obj1', 'obj2' ]
    content = 'asdasd'
    buckets = []

    # create bucket & object in all zones
    bucketA = create_zone_bucket(zcA)
    buckets.append(bucketA)
    create_object(zcA, bucketA, objnames[0], content)

    bucketB = create_zone_bucket(zcB)
    buckets.append(bucketB)
    create_object(zcB, bucketB, objnames[1], content)

    zonegroup_meta_checkpoint(zonegroup)
    # 'zonegroup_data_checkpoint' currently fails for the zones not
    # allowed to sync. So as a workaround, data checkpoint is done
    # for only the ones configured.
    zone_data_checkpoint(zoneB, zoneA)

    # verify if objects are synced accross the zone
    bucket = get_bucket(zcB, bucketA.name)
    check_object_exists(bucket, objnames[0], content)

    bucket = get_bucket(zcA, bucketB.name)
    check_object_exists(bucket, objnames[1], content)

    remove_sync_policy_group(c1, "sync-group")
    return

@attr('sync_policy')
def test_sync_flow_symmetrical_zonegroup_select():
    """
    test_sync_flow_symmetrical_zonegroup_select:
        allow sync between zoneA & zoneB
        verify zoneC doesnt sync the data
    """

    zonegroup = realm.master_zonegroup()
    zonegroup_conns = ZonegroupConns(zonegroup)

    if len(zonegroup.zones) < 3:
        raise SkipTest("test_sync_flow_symmetrical_zonegroup_select skipped. Requires 3 or more zones in master zonegroup.")

    zonegroup_meta_checkpoint(zonegroup)

    (zoneA, zoneB, zoneC) = zonegroup.zones[0:3]
    (zcA, zcB, zcC) = zonegroup_conns.zones[0:3]

    c1 = zoneA.cluster

    # configure sync policy
    zones = zoneA.name + ',' + zoneB.name
    c1.admin(['sync', 'policy', 'get'])
    create_sync_policy_group(c1, "sync-group")
    create_sync_group_flow_symmetrical(c1, "sync-group", "sync-flow", zones)
    create_sync_group_pipe(c1, "sync-group", "sync-pipe", zones, zones)
    set_sync_policy_group_status(c1, "sync-group", "enabled")

    zonegroup.period.update(zoneA, commit=True)
    get_sync_policy(c1)

    buckets = []
    content = 'asdasd'

    # create bucketA & objects in zoneA
    objnamesA = [ 'obj1', 'obj2', 'obj3' ]
    bucketA = create_zone_bucket(zcA)
    buckets.append(bucketA)
    create_objects(zcA, bucketA, objnamesA, content)

    # create bucketB & objects in zoneB
    objnamesB = [ 'obj4', 'obj5', 'obj6' ]
    bucketB = create_zone_bucket(zcB)
    buckets.append(bucketB)
    create_objects(zcB, bucketB, objnamesB, content)

    zonegroup_meta_checkpoint(zonegroup)
    zone_data_checkpoint(zoneB, zoneA)
    zone_data_checkpoint(zoneA, zoneB)

    # verify if objnamesA synced to only zoneB but not zoneC
    bucket = get_bucket(zcB, bucketA.name)
    check_objects_exist(bucket, objnamesA, content)

    bucket = get_bucket(zcC, bucketA.name)
    check_objects_not_exist(bucket, objnamesA)

    # verify if objnamesB synced to only zoneA but not zoneC
    bucket = get_bucket(zcA, bucketB.name)
    check_objects_exist(bucket, objnamesB, content)

    bucket = get_bucket(zcC, bucketB.name)
    check_objects_not_exist(bucket, objnamesB)

    remove_sync_policy_group(c1, "sync-group")
    return

@attr('sync_policy')
def test_sync_flow_directional_zonegroup_select():
    """
    test_sync_flow_directional_zonegroup_select:
        allow sync from only zoneA to zoneB
        
        verify that data doesn't get synced to zoneC and
        zoneA shouldn't sync data from zoneB either
    """

    zonegroup = realm.master_zonegroup()
    zonegroup_conns = ZonegroupConns(zonegroup)

    if len(zonegroup.zones) < 3:
        raise SkipTest("test_sync_flow_symmetrical_zonegroup_select skipped. Requires 3 or more zones in master zonegroup.")

    zonegroup_meta_checkpoint(zonegroup)

    (zoneA, zoneB, zoneC) = zonegroup.zones[0:3]
    (zcA, zcB, zcC) = zonegroup_conns.zones[0:3]

    c1 = zoneA.cluster

    # configure sync policy
    zones = zoneA.name + ',' + zoneB.name
    c1.admin(['sync', 'policy', 'get'])
    create_sync_policy_group(c1, "sync-group")
    create_sync_group_flow_directional(c1, "sync-group", "sync-flow", zoneA.name, zoneB.name)
    create_sync_group_pipe(c1, "sync-group", "sync-pipe", zoneA.name, zoneB.name)
    set_sync_policy_group_status(c1, "sync-group", "enabled")

    zonegroup.period.update(zoneA, commit=True)
    get_sync_policy(c1)

    buckets = []
    content = 'asdasd'

    # create bucketA & objects in zoneA
    objnamesA = [ 'obj1', 'obj2', 'obj3' ]
    bucketA = create_zone_bucket(zcA)
    buckets.append(bucketA)
    create_objects(zcA, bucketA, objnamesA, content)

    # create bucketB & objects in zoneB
    objnamesB = [ 'obj4', 'obj5', 'obj6' ]
    bucketB = create_zone_bucket(zcB)
    buckets.append(bucketB)
    create_objects(zcB, bucketB, objnamesB, content)

    zonegroup_meta_checkpoint(zonegroup)
    zone_data_checkpoint(zoneB, zoneA)

    # verify if objnamesA synced to only zoneB but not zoneC
    bucket = get_bucket(zcB, bucketA.name)
    check_objects_exist(bucket, objnamesA, content)

    bucket = get_bucket(zcC, bucketA.name)
    check_objects_not_exist(bucket, objnamesA)

    # verify if objnamesB are not synced to either zoneA or zoneC
    bucket = get_bucket(zcA, bucketB.name)
    check_objects_not_exist(bucket, objnamesB)

    bucket = get_bucket(zcC, bucketB.name)
    check_objects_not_exist(bucket, objnamesB)

    """
        verify the same at bucketA level
        configure another policy at bucketA level with src and dest
        zones specified to zoneA and zoneB resp.

        verify zoneA bucketA syncs to zoneB BucketA but not viceversa.
    """
    # reconfigure zonegroup pipe & flow
    remove_sync_group_pipe(c1, "sync-group", "sync-pipe")
    remove_sync_group_flow_directional(c1, "sync-group", "sync-flow", zoneA.name, zoneB.name)
    create_sync_group_flow_symmetrical(c1, "sync-group", "sync-flow1", zones)
    create_sync_group_pipe(c1, "sync-group", "sync-pipe", zones, zones)

    # change state to allowed
    set_sync_policy_group_status(c1, "sync-group", "allowed")

    zonegroup.period.update(zoneA, commit=True)
    get_sync_policy(c1)

    # configure sync policy for only bucketA and enable it
    create_sync_policy_group(c1, "sync-bucket", "allowed", bucketA.name)
    create_sync_group_flow_symmetrical(c1, "sync-bucket", "sync-flowA", zones, bucketA.name)
    args = ['--source-bucket=*', '--dest-bucket=*']
    create_sync_group_pipe(c1, "sync-bucket", "sync-pipe", zoneA.name, zoneB.name, bucketA.name, args)
    set_sync_policy_group_status(c1, "sync-bucket", "enabled", bucketA.name)

    get_sync_policy(c1, bucketA.name)

    zonegroup_meta_checkpoint(zonegroup)

    # create objects in bucketA in zoneA and zoneB
    objnamesC = [ 'obj7', 'obj8', 'obj9' ]
    objnamesD = [ 'obj10', 'obj11', 'obj12' ]
    create_objects(zcA, bucketA, objnamesC, content)
    create_objects(zcB, bucketA, objnamesD, content)

    zonegroup_meta_checkpoint(zonegroup)
    zone_data_checkpoint(zoneB, zoneA)

    # verify that objnamesC are synced to bucketA in zoneB
    bucket = get_bucket(zcB, bucketA.name)
    check_objects_exist(bucket, objnamesC, content)

    # verify that objnamesD are not synced to bucketA in zoneA
    bucket = get_bucket(zcA, bucketA.name)
    check_objects_not_exist(bucket, objnamesD)

    remove_sync_policy_group(c1, "sync-bucket", bucketA.name)
    remove_sync_policy_group(c1, "sync-group")
    return

@attr('sync_policy')
def test_sync_single_bucket():
    """
    test_sync_single_bucket:
        Allow data sync for only bucketA but not for other buckets via
        below 2 methods

        (a) zonegroup: symmetrical flow but configure pipe for only bucketA.
        (b) bucket level: configure policy for bucketA
    """

    zonegroup = realm.master_zonegroup()
    zonegroup_meta_checkpoint(zonegroup)

    zonegroup_conns = ZonegroupConns(zonegroup)

    (zoneA, zoneB) = zonegroup.zones[0:2]
    (zcA, zcB) = zonegroup_conns.zones[0:2]

    c1 = zoneA.cluster

    c1.admin(['sync', 'policy', 'get'])

    zones = zoneA.name + ',' + zoneB.name
    get_sync_policy(c1)

    objnames = [ 'obj1', 'obj2', 'obj3' ]
    content = 'asdasd'
    buckets = []

    # create bucketA & bucketB in zoneA
    bucketA = create_zone_bucket(zcA)
    buckets.append(bucketA)
    bucketB = create_zone_bucket(zcA)
    buckets.append(bucketB)

    zonegroup_meta_checkpoint(zonegroup)

    """
        Method (a): configure pipe for only bucketA
    """
    # configure sync policy & pipe for only bucketA
    create_sync_policy_group(c1, "sync-group")
    create_sync_group_flow_symmetrical(c1, "sync-group", "sync-flow1", zones)
    args = ['--source-bucket=' +  bucketA.name, '--dest-bucket=' + bucketA.name]

    create_sync_group_pipe(c1, "sync-group", "sync-pipe", zones, zones, None, args)
    set_sync_policy_group_status(c1, "sync-group", "enabled")
    get_sync_policy(c1)
    zonegroup.period.update(zoneA, commit=True)
    
    sync_info(c1)

    # create objects in bucketA & bucketB
    create_objects(zcA, bucketA, objnames, content)
    create_object(zcA, bucketB, objnames, content)

    zonegroup_meta_checkpoint(zonegroup)
    zone_data_checkpoint(zoneB, zoneA)

    # verify if bucketA objects are synced
    bucket = get_bucket(zcB, bucketA.name)
    check_objects_exist(bucket, objnames, content)

    # bucketB objects should not be synced
    bucket = get_bucket(zcB, bucketB.name)
    check_objects_not_exist(bucket, objnames)


    """
        Method (b): configure policy at only bucketA level 
    """
    # reconfigure group pipe
    remove_sync_group_pipe(c1, "sync-group", "sync-pipe")
    create_sync_group_pipe(c1, "sync-group", "sync-pipe", zones, zones)

    # change state to allowed
    set_sync_policy_group_status(c1, "sync-group", "allowed")

    zonegroup.period.update(zoneA, commit=True)
    get_sync_policy(c1)


    # configure sync policy for only bucketA and enable it
    create_sync_policy_group(c1, "sync-bucket", "allowed", bucketA.name)
    create_sync_group_flow_symmetrical(c1, "sync-bucket", "sync-flowA", zones, bucketA.name)
    create_sync_group_pipe(c1, "sync-bucket", "sync-pipe", zones, zones, bucketA.name)
    set_sync_policy_group_status(c1, "sync-bucket", "enabled", bucketA.name)

    get_sync_policy(c1, bucketA.name)

    # create object in bucketA
    create_object(zcA, bucketA, objnames[2], content)

    # create object in bucketA too
    create_object(zcA, bucketB, objnames[2], content)

    zonegroup_meta_checkpoint(zonegroup)
    zone_data_checkpoint(zoneB, zoneA)

    # verify if bucketA objects are synced
    bucket = get_bucket(zcB, bucketA.name)
    check_object_exists(bucket, objnames[2], content)

    # bucketB objects should not be synced
    bucket = get_bucket(zcB, bucketB.name)
    check_object_not_exists(bucket, objnames[2])

    remove_sync_policy_group(c1, "sync-bucket", bucketA.name)
    remove_sync_policy_group(c1, "sync-group")
    return

@attr('sync_policy')
def test_sync_different_buckets():
    """
    test_sync_different_buckets:
        sync zoneA bucketA to zoneB bucketB via below methods

        (a) zonegroup: directional flow but configure pipe for zoneA bucketA to zoneB bucketB
        (b) bucket: configure another policy at bucketA level with pipe set to
        another bucket(bucketB) in target zone.

        sync zoneA bucketA from zoneB bucketB
        (c) configure another policy at bucketA level with pipe set from
        another bucket(bucketB) in source zone.

    """

    zonegroup = realm.master_zonegroup()
    zonegroup_meta_checkpoint(zonegroup)

    zonegroup_conns = ZonegroupConns(zonegroup)

    (zoneA, zoneB) = zonegroup.zones[0:2]
    (zcA, zcB) = zonegroup_conns.zones[0:2]
    zones = zoneA.name + ',' + zoneB.name

    c1 = zoneA.cluster

    c1.admin(['sync', 'policy', 'get'])

    objnames = [ 'obj1', 'obj2' ]
    objnamesB = [ 'obj3', 'obj4' ]
    content = 'asdasd'
    buckets = []

    # create bucketA & bucketB in zoneA
    bucketA = create_zone_bucket(zcA)
    buckets.append(bucketA)
    bucketB = create_zone_bucket(zcA)
    buckets.append(bucketB)

    zonegroup_meta_checkpoint(zonegroup)

    """
        Method (a): zonegroup - configure pipe for only bucketA
    """
    # configure pipe from zoneA bucketA to zoneB bucketB
    create_sync_policy_group(c1, "sync-group")
    create_sync_group_flow_symmetrical(c1, "sync-group", "sync-flow1", zones)
    args = ['--source-bucket=' +  bucketA.name, '--dest-bucket=' + bucketB.name]
    create_sync_group_pipe(c1, "sync-group", "sync-pipe", zoneA.name, zoneB.name, None, args)
    set_sync_policy_group_status(c1, "sync-group", "enabled")
    zonegroup.period.update(zoneA, commit=True)
    get_sync_policy(c1)

    # create objects in bucketA
    create_objects(zcA, bucketA, objnames, content)

    zonegroup_meta_checkpoint(zonegroup)
    zone_data_checkpoint(zoneB, zoneA)

    # verify that objects are synced to bucketB in zoneB
    # but not to bucketA
    bucket = get_bucket(zcB, bucketA.name)
    check_objects_not_exist(bucket, objnames)

    bucket = get_bucket(zcB, bucketB.name)
    check_objects_exist(bucket, objnames, content)
    """
        Method (b): configure policy at only bucketA level with pipe
        set to bucketB in target zone
    """

    remove_sync_group_pipe(c1, "sync-group", "sync-pipe")
    create_sync_group_pipe(c1, "sync-group", "sync-pipe", zones, zones)

    # change state to allowed
    set_sync_policy_group_status(c1, "sync-group", "allowed")

    zonegroup.period.update(zoneA, commit=True)
    get_sync_policy(c1)

    # configure sync policy for only bucketA and enable it
    create_sync_policy_group(c1, "sync-bucket", "allowed", bucketA.name)
    create_sync_group_flow_symmetrical(c1, "sync-bucket", "sync-flowA", zones, bucketA.name)
    args = ['--source-bucket=*', '--dest-bucket=' + bucketB.name]
    create_sync_group_pipe(c1, "sync-bucket", "sync-pipeA", zones, zones, bucketA.name, args)
    set_sync_policy_group_status(c1, "sync-bucket", "enabled", bucketA.name)

    get_sync_policy(c1, bucketA.name)

    objnamesC = [ 'obj5', 'obj6' ]

    zonegroup_meta_checkpoint(zonegroup)
    # create objects in bucketA
    create_objects(zcA, bucketA, objnamesC, content)

    zonegroup_meta_checkpoint(zonegroup)
    zone_data_checkpoint(zoneB, zoneA)

    """
    # verify that objects are synced to bucketB in zoneB
    # but not to bucketA
    """
    bucket = get_bucket(zcB, bucketA.name)
    check_objects_not_exist(bucket, objnamesC)

    bucket = get_bucket(zcB, bucketB.name)
    check_objects_exist(bucket, objnamesC, content)

    remove_sync_policy_group(c1, "sync-bucket", bucketA.name)
    zonegroup_meta_checkpoint(zonegroup)
    get_sync_policy(c1, bucketA.name)

    """
        Method (c): configure policy at only bucketA level with pipe
        set from bucketB in source zone
        verify zoneA bucketA syncs from zoneB BucketB but not bucketA
    """

    # configure sync policy for only bucketA and enable it
    create_sync_policy_group(c1, "sync-bucket", "allowed", bucketA.name)
    create_sync_group_flow_symmetrical(c1, "sync-bucket", "sync-flowA", zones, bucketA.name)
    args = ['--source-bucket=' +  bucketB.name, '--dest-bucket=' + '*']
    create_sync_group_pipe(c1, "sync-bucket", "sync-pipe", zones, zones, bucketA.name, args)
    set_sync_policy_group_status(c1, "sync-bucket", "enabled", bucketA.name)

    get_sync_policy(c1, bucketA.name)

    # create objects in bucketA & B in ZoneB
    objnamesD = [ 'obj7', 'obj8' ]
    objnamesE = [ 'obj9', 'obj10' ]

    create_objects(zcB, bucketA, objnamesD, content)
    create_objects(zcB, bucketB, objnamesE, content)

    zonegroup_meta_checkpoint(zonegroup)
    zone_data_checkpoint(zoneA, zoneB)
    """
    # verify that objects from only bucketB are synced to
    # bucketA in zoneA
    """
    bucket = get_bucket(zcA, bucketA.name)
    check_objects_not_exist(bucket, objnamesD)
    check_objects_exist(bucket, objnamesE, content)

    remove_sync_policy_group(c1, "sync-bucket", bucketA.name)
    remove_sync_policy_group(c1, "sync-group")
    return

@attr('sync_policy')
def test_sync_multiple_buckets_to_single():
    """
    test_sync_multiple_buckets_to_single:
        directional flow
        (a) pipe: sync zoneA bucketA,bucketB to zoneB bucketB

        (b) configure another policy at bucketA level with pipe configured
        to sync from multiple buckets (bucketA & bucketB)

        verify zoneA bucketA & bucketB syncs to zoneB BucketB
    """

    zonegroup = realm.master_zonegroup()
    zonegroup_meta_checkpoint(zonegroup)

    zonegroup_conns = ZonegroupConns(zonegroup)

    (zoneA, zoneB) = zonegroup.zones[0:2]
    (zcA, zcB) = zonegroup_conns.zones[0:2]
    zones = zoneA.name + ',' + zoneB.name

    c1 = zoneA.cluster

    c1.admin(['sync', 'policy', 'get'])

    objnamesA = [ 'obj1', 'obj2' ]
    objnamesB = [ 'obj3', 'obj4' ]
    content = 'asdasd'
    buckets = []

    # create bucketA & bucketB in zoneA
    bucketA = create_zone_bucket(zcA)
    buckets.append(bucketA)
    bucketB = create_zone_bucket(zcA)
    buckets.append(bucketB)

    zonegroup_meta_checkpoint(zonegroup)

    # configure pipe from zoneA bucketA,bucketB to zoneB bucketB
    create_sync_policy_group(c1, "sync-group")
    create_sync_group_flow_directional(c1, "sync-group", "sync-flow", zoneA.name, zoneB.name)
    source_buckets = [ bucketA.name, bucketB.name ]
    for source_bucket in source_buckets:
        args = ['--source-bucket=' +  source_bucket, '--dest-bucket=' + bucketB.name]
        create_sync_group_pipe(c1, "sync-group", "sync-pipe-%s" % source_bucket, zoneA.name, zoneB.name, None, args)

    set_sync_policy_group_status(c1, "sync-group", "enabled")
    zonegroup.period.update(zoneA, commit=True)
    get_sync_policy(c1)

    # create objects in bucketA & bucketB
    create_objects(zcA, bucketA, objnamesA, content)
    create_objects(zcA, bucketB, objnamesB, content)

    zonegroup_meta_checkpoint(zonegroup)
    zone_data_checkpoint(zoneB, zoneA)

    # verify that both zoneA bucketA & bucketB objects are synced to
    # bucketB in zoneB but not to bucketA
    bucket = get_bucket(zcB, bucketA.name)
    check_objects_not_exist(bucket, objnamesA)
    check_objects_not_exist(bucket, objnamesB)

    bucket = get_bucket(zcB, bucketB.name)
    check_objects_exist(bucket, objnamesA, content)
    check_objects_exist(bucket, objnamesB, content)

    """
        Method (b): configure at bucket level
    """
    # reconfigure pipe & flow
    for source_bucket in source_buckets:
        remove_sync_group_pipe(c1, "sync-group", "sync-pipe-%s" % source_bucket)
    remove_sync_group_flow_directional(c1, "sync-group", "sync-flow", zoneA.name, zoneB.name)
    create_sync_group_flow_symmetrical(c1, "sync-group", "sync-flow1", zones)
    create_sync_group_pipe(c1, "sync-group", "sync-pipe", zones, zones)

    # change state to allowed
    set_sync_policy_group_status(c1, "sync-group", "allowed")

    zonegroup.period.update(zoneA, commit=True)
    get_sync_policy(c1)

    objnamesC = [ 'obj5', 'obj6' ]
    objnamesD = [ 'obj7', 'obj8' ]

    # configure sync policy for only bucketA and enable it
    create_sync_policy_group(c1, "sync-bucket", "allowed", bucketA.name)
    create_sync_group_flow_symmetrical(c1, "sync-bucket", "sync-flowA", zones, bucketA.name)
    source_buckets = [ bucketA.name, bucketB.name ]
    for source_bucket in source_buckets:
        args = ['--source-bucket=' +  source_bucket, '--dest-bucket=' + '*']
        create_sync_group_pipe(c1, "sync-bucket", "sync-pipe-%s" % source_bucket, zoneA.name, zoneB.name, bucketA.name, args)

    set_sync_policy_group_status(c1, "sync-bucket", "enabled", bucketA.name)

    get_sync_policy(c1)

    zonegroup_meta_checkpoint(zonegroup)
    # create objects in bucketA
    create_objects(zcA, bucketA, objnamesC, content)
    create_objects(zcA, bucketB, objnamesD, content)

    zonegroup_meta_checkpoint(zonegroup)
    zone_data_checkpoint(zoneB, zoneA)

    # verify that both zoneA bucketA & bucketB objects are synced to
    # bucketA in zoneB but not to bucketB
    bucket = get_bucket(zcB, bucketB.name)
    check_objects_not_exist(bucket, objnamesC)
    check_objects_not_exist(bucket, objnamesD)

    bucket = get_bucket(zcB, bucketA.name)
    check_objects_exist(bucket, objnamesD, content)
    check_objects_exist(bucket, objnamesD, content)

    remove_sync_policy_group(c1, "sync-bucket", bucketA.name)
    remove_sync_policy_group(c1, "sync-group")
    return

@attr('sync_policy')
def test_sync_single_bucket_to_multiple():
    """
    test_sync_single_bucket_to_multiple:
        directional flow
        (a) pipe: sync zoneA bucketA to zoneB bucketA & bucketB

        (b) configure another policy at bucketA level with pipe configured
        to sync to multiple buckets (bucketA & bucketB)

        verify zoneA bucketA syncs to zoneB bucketA & bucketB
    """

    zonegroup = realm.master_zonegroup()
    zonegroup_meta_checkpoint(zonegroup)

    zonegroup_conns = ZonegroupConns(zonegroup)

    (zoneA, zoneB) = zonegroup.zones[0:2]
    (zcA, zcB) = zonegroup_conns.zones[0:2]
    zones = zoneA.name + ',' + zoneB.name

    c1 = zoneA.cluster

    c1.admin(['sync', 'policy', 'get'])

    objnamesA = [ 'obj1', 'obj2' ]
    content = 'asdasd'
    buckets = []

    # create bucketA & bucketB in zoneA
    bucketA = create_zone_bucket(zcA)
    buckets.append(bucketA)
    bucketB = create_zone_bucket(zcA)
    buckets.append(bucketB)

    zonegroup_meta_checkpoint(zonegroup)

    # configure pipe from zoneA bucketA to zoneB bucketA, bucketB
    create_sync_policy_group(c1, "sync-group")
    create_sync_group_flow_symmetrical(c1, "sync-group", "sync-flow1", zones)

    dest_buckets = [ bucketA.name, bucketB.name ]
    for dest_bucket in dest_buckets:
        args = ['--source-bucket=' +  bucketA.name, '--dest-bucket=' + dest_bucket]
        create_sync_group_pipe(c1, "sync-group", "sync-pipe-%s" % dest_bucket, zoneA.name, zoneB.name, None, args)

    create_sync_group_pipe(c1, "sync-group", "sync-pipe", zoneA.name, zoneB.name, None, args)
    set_sync_policy_group_status(c1, "sync-group", "enabled")
    zonegroup.period.update(zoneA, commit=True)
    get_sync_policy(c1)

    # create objects in bucketA
    create_objects(zcA, bucketA, objnamesA, content)

    zonegroup_meta_checkpoint(zonegroup)
    zone_data_checkpoint(zoneB, zoneA)

    # verify that objects from zoneA bucketA are synced to both
    # bucketA & bucketB in zoneB
    bucket = get_bucket(zcB, bucketA.name)
    check_objects_exist(bucket, objnamesA, content)

    bucket = get_bucket(zcB, bucketB.name)
    check_objects_exist(bucket, objnamesA, content)

    """
        Method (b): configure at bucket level
    """
    remove_sync_group_pipe(c1, "sync-group", "sync-pipe")
    create_sync_group_pipe(c1, "sync-group", "sync-pipe", '*', '*')

    # change state to allowed
    set_sync_policy_group_status(c1, "sync-group", "allowed")

    zonegroup.period.update(zoneA, commit=True)
    get_sync_policy(c1)

    objnamesB = [ 'obj3', 'obj4' ]

    # configure sync policy for only bucketA and enable it
    create_sync_policy_group(c1, "sync-bucket", "allowed", bucketA.name)
    create_sync_group_flow_symmetrical(c1, "sync-bucket", "sync-flowA", zones, bucketA.name)
    dest_buckets = [ bucketA.name, bucketB.name ]
    for dest_bucket in dest_buckets:
        args = ['--source-bucket=' + '*', '--dest-bucket=' + dest_bucket]
        create_sync_group_pipe(c1, "sync-bucket", "sync-pipe-%s" % dest_bucket, zoneA.name, zoneB.name, bucketA.name, args)

    set_sync_policy_group_status(c1, "sync-bucket", "enabled", bucketA.name)

    get_sync_policy(c1)

    zonegroup_meta_checkpoint(zonegroup)
    # create objects in bucketA
    create_objects(zcA, bucketA, objnamesB, content)

    zonegroup_meta_checkpoint(zonegroup)
    zone_data_checkpoint(zoneB, zoneA)

    # verify that objects from zoneA bucketA are synced to both
    # bucketA & bucketB in zoneB
    bucket = get_bucket(zcB, bucketA.name)
    check_objects_exist(bucket, objnamesB, content)

    bucket = get_bucket(zcB, bucketB.name)
    check_objects_exist(bucket, objnamesB, content)

    remove_sync_policy_group(c1, "sync-bucket", bucketA.name)
    remove_sync_policy_group(c1, "sync-group")
    return
