Esempio n. 1
0
    def test_retain_copy(self, session_mock):
        settings = {
            'aws_bucket': self.aws_bucket,
            'aws_identity': 'identity',
            'aws_secret': 'credential',
            'account': 'account',
            'container': 'container',
            'retain_local': False
        }

        sync = SyncContainer(self.scratch_space, settings)
        sync.provider = mock.Mock()
        swift_client = mock.Mock()
        row = {
            'deleted': 0,
            'created_at': str(time.time() - 5),
            'name': 'foo',
            'storage_policy_index': 99
        }
        sync.handle(row, swift_client)

        _, _, swift_ts = decode_timestamps(row['created_at'])
        swift_ts.offset += 1

        sync.provider.upload_object.assert_called_once_with(
            row['name'], 99, swift_client)
        swift_client.delete_object.assert_called_once_with(
            settings['account'],
            settings['container'],
            row['name'],
            headers={'X-Timestamp': Timestamp(swift_ts).internal})
Esempio n. 2
0
    def test_retain_copy(self, session_mock):
        settings = {
            'aws_bucket': self.aws_bucket,
            'aws_identity': 'identity',
            'aws_secret': 'credential',
            'account': 'account',
            'container': 'container',
            'retain_local': False
        }

        sync = SyncContainer(self.scratch_space, settings, self.stats_factory)
        sync.provider = mock.Mock()
        sync.provider.upload_object.return_value = SyncS3.UploadStatus.PUT
        swift_client = mock.Mock()
        swift_client.get_object_metadata.return_value = {}
        row = {
            'deleted': 0,
            'created_at': str(time.time() - 5),
            'name': 'foo',
            'storage_policy_index': 99
        }
        sync.handle(row, swift_client)

        _, _, swift_ts = decode_timestamps(row['created_at'])

        sync.provider.upload_object.assert_called_once_with(
            row, swift_client, mock.ANY)
        sync.provider.delete_local_object.assert_called_once_with(
            swift_client, row, swift_ts, False)
        sync.stats_reporter.increment.assert_called_once_with(
            'copied_objects', 1)
Esempio n. 3
0
    def test_retry_copy_after(self, session_mock):
        settings = {
            'aws_bucket': self.aws_bucket,
            'aws_identity': 'identity',
            'aws_secret': 'credential',
            'account': 'account',
            'container': 'container',
            'copy_after': 3600
        }
        with self.assertRaises(RetryError):
            sync = SyncContainer(self.scratch_space, settings)
            sync.handle({'deleted': 0, 'created_at': str(time.time())}, None)

        current = time.time()
        with mock.patch('s3_sync.sync_container.time') as time_mock:
            time_mock.time.return_value = current + settings['copy_after'] + 1
            sync = SyncContainer(self.scratch_space, settings)
            sync.provider = mock.Mock()
            sync.handle(
                {
                    'deleted': 0,
                    'created_at': str(time.time()),
                    'name': 'foo',
                    'storage_policy_index': 99
                }, None)
            sync.provider.upload_object.assert_called_once_with(
                'foo', 99, None)
Esempio n. 4
0
 def test_skips_matching_exclude_rows(self):
     settings = {
         'aws_bucket': self.aws_bucket,
         'aws_identity': 'identity',
         'aws_secret': 'credential',
         'account': 'account',
         'container': 'container',
         'exclude_pattern': '^foo-\d+$'
     }
     sync = SyncContainer(self.scratch_space, settings, self.stats_factory)
     sync.provider = mock.Mock()
     sync.handle({'name': 'foo-1'}, mock.Mock())
     sync.handle({'name': 'foo-12345', 'deleted': 1}, mock.Mock())
     sync.handle(
         {
             'name': 'foo-1-bar',
             'deleted': 0,
             'created_at': '123456789.1234'
         }, mock.Mock())
     self.assertEqual([
         mock.call.upload_object(
             {
                 'name': 'foo-1-bar',
                 'deleted': 0,
                 'created_at': '123456789.1234'
             }, mock.ANY, mock.ANY)
     ], sync.provider.mock_calls)
Esempio n. 5
0
    def test_handle_container_update_metadata(self, mock_swift, mock_exists,
                                              mock_open):
        mock_exists.return_value = True
        old_hash = hash_dict({'X-Container-Meta': 'foo'})
        db_entries = {
            'db-id-1': {
                'aws_bucket': 'bucket',
                'last_row': 5,
                SyncContainer.METADATA_HASH_KEY: old_hash
            }
        }
        fake_conf_file = self.MockMetaConf(db_entries)
        mock_open.return_value = fake_conf_file

        settings = {
            'aws_bucket': self.aws_bucket,
            'aws_identity': 'identity',
            'aws_secret': 'credential',
            'aws_endpoint': 'http://example.com',
            'account': 'account',
            'sync_container_metadata': True,
            'container': 'container',
            'protocol': 'swift'
        }

        sync = SyncContainer(self.scratch_space, settings, self.stats_factory)
        metadata = {
            'X-Container-Meta-Foo': ('foo', '1545160000.1234'),
            'X-Container-Meta-Bar': ('bar', '1546123456.7896'),
            'Some-Other-Key': ('val', '1545179217.201453'),
            'X-Versions-Locations': ('versions', '1545179217.201453')
        }
        mock_swift.return_value.post_container.return_value = {}

        sync.handle_container_info({'id': 'db-id-1'}, metadata)
        mock_swift.assert_called_once_with(authurl='http://example.com',
                                           key='credential',
                                           user='******',
                                           os_options={},
                                           retries=3)
        self.assertEqual([
            mock.call.post_container(self.aws_bucket,
                                     headers={
                                         'X-Container-Meta-Foo': 'foo',
                                         'X-Container-Meta-Bar': 'bar'
                                     })
        ], mock_swift.return_value.mock_calls)
Esempio n. 6
0
    def setUp(self, mock_boto3):
        self.mock_boto3_session = mock.Mock()
        self.mock_boto3_client = mock.Mock()

        mock_boto3.return_value = self.mock_boto3_session
        self.mock_boto3_session.client.return_value = self.mock_boto3_client

        self.aws_bucket = 'bucket'
        self.scratch_space = 'scratch'
        self.sync_container = SyncContainer(
            self.scratch_space, {
                'aws_bucket': self.aws_bucket,
                'aws_identity': 'identity',
                'aws_secret': 'credential',
                'account': 'account',
                'container': 'container'
            })
Esempio n. 7
0
    def test_no_propagate_delete(self, session_mock):
        settings = {
            'aws_bucket': self.aws_bucket,
            'aws_identity': 'identity',
            'aws_secret': 'credential',
            'account': 'account',
            'container': 'container',
            'propagate_delete': False
        }

        sync = SyncContainer(self.scratch_space, settings)
        sync.provider = mock.Mock()
        row = {'deleted': 1, 'name': 'tombstone'}
        sync.handle(row, None)

        # Make sure we do nothing with this row
        self.assertEqual([], sync.provider.mock_calls)
Esempio n. 8
0
    def test_propagate_delete(self, session_mock):
        settings = {
            'aws_bucket': self.aws_bucket,
            'aws_identity': 'identity',
            'aws_secret': 'credential',
            'account': 'account',
            'container': 'container',
            'propagate_delete': True
        }

        sync = SyncContainer(self.scratch_space, settings)
        sync.provider = mock.Mock()
        row = {'deleted': 1, 'name': 'tombstone'}
        sync.handle(row, None)

        # Make sure that we do not make any additional calls
        self.assertEqual([mock.call.delete_object(row['name'])],
                         sync.provider.mock_calls)
Esempio n. 9
0
 def test_unknown_provider(self):
     settings = {
         'aws_bucket': self.aws_bucket,
         'aws_identity': 'identity',
         'aws_secret': 'credential',
         'account': 'account',
         'container': 'container',
         'protocol': 'foo'
     }
     with self.assertRaises(NotImplementedError):
         SyncContainer(self.scratch_space, settings, 1)
Esempio n. 10
0
    def test_handle_container_same_metadata(self, mock_swift, mock_exists,
                                            mock_open):
        mock_exists.return_value = True
        metadata = {
            'X-Container-Meta-Foo': ('foo', '1545160000.1234'),
            'X-Container-Meta-Bar': ('bar', '1546123456.7896'),
            'Some-Other-Key': ('val', '1545179217.201453'),
            'X-Versions-Locations': ('versions', '1545179217.201453')
        }
        meta_hash = hash_dict({
            'X-Container-Meta-Foo': 'foo',
            'X-Container-Meta-Bar': 'bar'
        })
        db_entries = {
            'db-id-1': {
                'aws_bucket': 'bucket',
                'last_row': 5,
                SyncContainer.METADATA_HASH_KEY: meta_hash
            }
        }
        fake_conf_file = self.MockMetaConf(db_entries)
        mock_open.return_value = fake_conf_file

        settings = {
            'aws_bucket': self.aws_bucket,
            'aws_identity': 'identity',
            'aws_endpoint': 'http://example.com',
            'aws_secret': 'credential',
            'account': 'account',
            'sync_container_metadata': True,
            'container': 'container',
            'protocol': 'swift'
        }

        sync = SyncContainer(self.scratch_space, settings, self.stats_factory)

        sync.handle_container_info({'id': 'db-id-1'}, metadata)
        # Connections are lazy-initialized
        mock_swift.assert_not_called()
Esempio n. 11
0
 def test_swift_provider(self):
     settings = {
         'aws_bucket': self.aws_bucket,
         'aws_identity': 'identity',
         'aws_secret': 'credential',
         'aws_endpoint': 'http://swift.example.com:8080/auth/v1.0',
         'account': 'account',
         'container': 'container',
         'protocol': 'swift'
     }
     sync = SyncContainer(self.scratch_space, settings, max_conns=1)
     self.assertIsInstance(sync.provider, SyncSwift)
     self.assertEqual(sync.provider.settings, settings)
     self.assertEqual(len(sync.provider.client_pool.client_pool), 0)
     self.assertEqual(sync.provider.client_pool.pool_size, 1)
Esempio n. 12
0
    def test_handle_container_info_errors(self, mock_swift):
        db_entries = {'db-id-1': {'aws_bucket': 'bucket', 'last_row': 5}}
        settings = {
            'aws_bucket': self.aws_bucket,
            'aws_identity': 'identity',
            'aws_secret': 'credential',
            'aws_endpoint': 'http://example.com',
            'account': 'account',
            'sync_container_metadata': True,
            'container': 'container',
            'protocol': 'swift'
        }

        metadata = {
            'X-Container-Meta-Foo': ('foo', '1545179217.201453'),
            'X-Delete-At': ('1645179217', '1545179217.201453'),
            'Some-Other-Key': ('val', '1545179217.201453'),
            'X-Versions-Locations': ('versions', '1545179217.201453'),
            'X-Container-Meta-Bar': ('bar', '1545179217.201453'),
        }

        tmpdir = tempfile.mkdtemp()
        try:
            os.makedirs(os.path.join(tmpdir, 'account'))
            with open(os.path.join(tmpdir, 'account', 'container'), 'w') as fh:
                fh.write(json.dumps(db_entries))

            sync = SyncContainer(tmpdir, settings, self.stats_factory)
            sync.logger = mock.Mock()
            mock_swift.return_value.post_container.side_effect = RuntimeError(
                'failed to post container')
            sync.handle_container_info({'id': 'db-id-1'}, metadata)
            sync.logger.error.assert_called_once_with(mock.ANY)
            sync.logger.debug.assert_called_once_with(mock.ANY)
        finally:
            shutil.rmtree(tmpdir)
Esempio n. 13
0
    def test_s3_provider(self):
        defaults = {
            'aws_bucket': self.aws_bucket,
            'aws_identity': 'identity',
            'aws_secret': 'credential',
            'account': 'account',
            'container': 'container'
        }
        test_settings = [
            defaults, dict(defaults.items() + [('protocol', 's3')])
        ]

        for settings in test_settings:
            sync = SyncContainer(self.scratch_space, settings, max_conns=1)
            self.assertIsInstance(sync.provider, SyncS3)
            self.assertEqual(sync.provider.settings, settings)
            self.assertEqual(len(sync.provider.client_pool.client_pool), 0)
            self.assertEqual(sync.provider.client_pool.pool_size, 1)
Esempio n. 14
0
class TestSyncContainer(unittest.TestCase):
    class MockMetaConf(object):
        def __init__(self, fake_status):
            self.fake_status = fake_status
            self.write_buf = ''

        def read(self, size=-1):
            if size != -1:
                raise RuntimeError()
            return json.dumps(self.fake_status)

        def write(self, data):
            # Only support write at the beginning
            self.write_buf += data

        def truncate(self, size=None):
            if size:
                raise RuntimeError('Not supported')
            self.fake_status = json.loads(self.write_buf)
            self.write_buf = ''

        def __exit__(self, *args):
            if self.write_buf:
                self.fake_status = json.loads(self.write_buf)
                self.write_buf = ''

        def __enter__(self):
            return self

        def seek(self, offset, flags=None):
            if offset != 0:
                raise RuntimeError

    @mock.patch('s3_sync.sync_s3.boto3.session.Session')
    def setUp(self, mock_boto3):
        self.mock_boto3_session = mock.Mock()
        self.mock_boto3_client = mock.Mock()

        mock_boto3.return_value = self.mock_boto3_session
        self.mock_boto3_session.client.return_value = self.mock_boto3_client

        self.aws_bucket = 'bucket'
        self.scratch_space = 'scratch'
        self.sync_container = SyncContainer(
            self.scratch_space, {
                'aws_bucket': self.aws_bucket,
                'aws_identity': 'identity',
                'aws_secret': 'credential',
                'account': 'account',
                'container': 'container'
            })

    def test_load_non_existent_meta(self):
        ret = self.sync_container.get_last_row('db-id')
        self.assertEqual(0, ret)

    @mock.patch('__builtin__.open')
    @mock.patch('s3_sync.sync_container.os.path.exists')
    def test_load_upgrade_status(self, mock_exists, mock_open):
        mock_exists.return_value = True
        fake_status = dict(last_row=42)
        mock_open.return_value = self.MockMetaConf(fake_status)

        status = self.sync_container.get_last_row('db-id')
        self.assertEqual(fake_status['last_row'], status)

        mock_exists.assert_called_with(
            '%s/%s/%s' % (self.scratch_space, self.sync_container._account,
                          self.sync_container._container))

    @mock.patch('__builtin__.open')
    @mock.patch('s3_sync.sync_container.os.path.exists')
    def test_last_row_new_bucket(self, mock_exists, mock_open):
        db_id = 'db-id-test'
        new_bucket = 'new-bucket'
        self.sync_container.aws_bucket = 'bucket'
        fake_status = {db_id: dict(last_row=42, aws_bucket=new_bucket)}

        mock_exists.return_value = True
        mock_open.return_value = self.MockMetaConf(fake_status)

        status = self.sync_container.get_last_row(db_id)
        self.assertEqual(0, status)

        mock_exists.assert_called_with(
            '%s/%s/%s' % (self.scratch_space, self.sync_container._account,
                          self.sync_container._container))

    @mock.patch('__builtin__.open')
    @mock.patch('s3_sync.sync_container.os.path.exists')
    def test_last_row_new_db_id(self, mock_exists, mock_open):
        db_id = 'db-id-test'
        self.sync_container.aws_bucket = 'bucket'
        fake_status = {db_id: dict(last_row=42, aws_bucket='bucket')}

        mock_exists.return_value = True
        mock_open.return_value = self.MockMetaConf(fake_status)

        status = self.sync_container.get_last_row('other-db-id')
        self.assertEqual(0, status)

        mock_exists.assert_called_with(
            '%s/%s/%s' % (self.scratch_space, self.sync_container._account,
                          self.sync_container._container))

    @mock.patch('__builtin__.open')
    @mock.patch('s3_sync.sync_container.os.path.exists')
    def test_last_row_new_policy(self, mock_exists, mock_open):
        db_id = 'db-id-test'
        for field in SyncContainer.POLICY_FIELDS:
            if field != 'copy_after':
                setattr(self.sync_container, field, False)
            else:
                setattr(self.sync_container, field, 42)
        fake_status = {
            db_id:
            dict(last_row=42,
                 aws_bucket='bucket',
                 policy=dict(retain_local=True,
                             propagate_delete=True,
                             copy_after=0))
        }

        mock_exists.return_value = True
        mock_open.return_value = self.MockMetaConf(fake_status)

        status = self.sync_container.get_last_row(db_id)
        self.assertEqual(0, status)

        mock_exists.assert_called_with(
            '%s/%s/%s' % (self.scratch_space, self.sync_container._account,
                          self.sync_container._container))

    @mock.patch('__builtin__.open')
    @mock.patch('s3_sync.sync_container.os.path.exists')
    def test_last_row_old_policy(self, mock_exists, mock_open):
        db_id = 'db-id-test'
        fake_status = {
            db_id:
            dict(last_row=42,
                 aws_bucket='bucket',
                 policy=dict(retain_local=True,
                             propagate_delete=True,
                             copy_after=0))
        }

        mock_exists.return_value = True
        mock_open.return_value = self.MockMetaConf(fake_status)

        status = self.sync_container.get_last_row(db_id)
        self.assertEqual(42, status)

        mock_exists.assert_called_with(
            '%s/%s/%s' % (self.scratch_space, self.sync_container._account,
                          self.sync_container._container))

    @mock.patch('__builtin__.open')
    @mock.patch('s3_sync.sync_container.os.path.exists')
    def test_last_row(self, mock_exists, mock_open):
        db_entries = [{
            'id': 'db-id-1',
            'aws_bucket': 'bucket',
            'last_row': 5
        }, {
            'id': 'db-id-2',
            'aws_bucket': 'bucket',
            'last_row': 7
        }]
        for entry in db_entries:
            self.sync_container.aws_bucket = entry['aws_bucket']
            fake_status = {
                entry['id']:
                dict(last_row=entry['last_row'],
                     aws_bucket=entry['aws_bucket'])
            }

            mock_exists.return_value = True
            mock_open.return_value = self.MockMetaConf(fake_status)

            status = self.sync_container.get_last_row(entry['id'])
            self.assertEqual(entry['last_row'], status)

            mock_exists.assert_called_with(
                '%s/%s/%s' % (self.scratch_space, self.sync_container._account,
                              self.sync_container._container))

    @mock.patch('__builtin__.open')
    def test_save_last_row(self, mock_open):
        db_entries = {
            'db-id-1': {
                'aws_bucket': 'bucket',
                'last_row': 5
            },
            'db-id-2': {
                'aws_bucket': 'bucket',
                'last_row': 7
            }
        }
        new_row = 42
        for db_id, entry in db_entries.items():
            self.sync_container.aws_bucket = entry['aws_bucket']
            fake_conf_file = self.MockMetaConf(db_entries)
            mock_open.return_value = fake_conf_file

            with mock.patch('s3_sync.sync_container.os.path.exists')\
                    as mock_exists:
                mock_exists.return_value = True

                self.sync_container.save_last_row(new_row, db_id)
                file_entries = fake_conf_file.fake_status
                for file_db_id, status in file_entries.items():
                    if file_db_id == db_id:
                        self.assertEqual(new_row, status['last_row'])
                    else:
                        self.assertEqual(db_entries[file_db_id]['last_row'],
                                         status['last_row'])
                    if db_id != file_db_id:
                        continue
                    else:
                        self.assertIn('policy', status)
                    for field in SyncContainer.POLICY_FIELDS:
                        self.assertEqual(status['policy'][field],
                                         getattr(self.sync_container, field))

                self.assertEqual([
                    mock.call(
                        '%s/%s' %
                        (self.scratch_space, self.sync_container._account)),
                    mock.call(
                        '%s/%s/%s' %
                        (self.scratch_space, self.sync_container._account,
                         self.sync_container._container))
                ], mock_exists.call_args_list)

    @mock.patch('__builtin__.open')
    @mock.patch('s3_sync.sync_container.os.path.exists')
    def test_save_no_prior_status(self, mock_exists, mock_open):
        def existence_check(path):
            if path == '%s/%s' % (self.scratch_space,
                                  self.sync_container._account):
                return True
            elif path == '%s/%s/%s' % (self.scratch_space,
                                       self.sync_container._account,
                                       self.sync_container._container):
                return False
            else:
                raise RuntimeError('Invalid path')

        self.sync_container.aws_bucket = 'bucket'
        fake_conf_file = self.MockMetaConf({})
        mock_exists.side_effect = existence_check
        mock_open.return_value = fake_conf_file

        self.sync_container.save_last_row(42, 'db-id')
        self.assertEqual(42, fake_conf_file.fake_status['db-id']['last_row'])
        self.assertEqual('bucket',
                         fake_conf_file.fake_status['db-id']['aws_bucket'])

        self.assertEqual([
            mock.call('%s/%s' %
                      (self.scratch_space, self.sync_container._account)),
            mock.call('%s/%s/%s' %
                      (self.scratch_space, self.sync_container._account,
                       self.sync_container._container))
        ], mock_exists.call_args_list)

    @mock.patch('__builtin__.open')
    @mock.patch('s3_sync.sync_container.os.path.exists')
    def test_save_last_row_new_bucket(self, mock_exists, mock_open):
        db_entries = {
            'db-id-1': {
                'aws_bucket': 'bucket',
                'last_row': 5
            },
            'db-id-2': {
                'aws_bucket': 'old-bucket',
                'last_row': 7
            }
        }
        new_row = 42
        for db_id, entry in db_entries.items():
            self.sync_container.aws_bucket = 'bucket'
            fake_conf_file = self.MockMetaConf(db_entries)
            mock_open.return_value = fake_conf_file

            with mock.patch('s3_sync.sync_container.os.path.exists')\
                    as mock_exists:
                mock_exists.return_value = True
                self.sync_container.save_last_row(new_row, db_id)
                file_entries = fake_conf_file.fake_status
                for file_db_id, status in file_entries.items():
                    if file_db_id == db_id:
                        self.assertEqual(new_row, status['last_row'])
                        self.assertEqual('bucket', status['aws_bucket'])
                    else:
                        self.assertEqual(db_entries[file_db_id]['last_row'],
                                         status['last_row'])
                        self.assertEqual(db_entries[file_db_id]['aws_bucket'],
                                         status['aws_bucket'])

                self.assertEqual([
                    mock.call(
                        '%s/%s' %
                        (self.scratch_space, self.sync_container._account)),
                    mock.call(
                        '%s/%s/%s' %
                        (self.scratch_space, self.sync_container._account,
                         self.sync_container._container))
                ], mock_exists.call_args_list)

    def test_s3_provider(self):
        defaults = {
            'aws_bucket': self.aws_bucket,
            'aws_identity': 'identity',
            'aws_secret': 'credential',
            'account': 'account',
            'container': 'container'
        }
        test_settings = [
            defaults, dict(defaults.items() + [('protocol', 's3')])
        ]

        for settings in test_settings:
            sync = SyncContainer(self.scratch_space, settings, max_conns=1)
            self.assertIsInstance(sync.provider, SyncS3)
            self.assertEqual(sync.provider.settings, settings)
            self.assertEqual(len(sync.provider.client_pool.client_pool), 0)
            self.assertEqual(sync.provider.client_pool.pool_size, 1)

    def test_swift_provider(self):
        settings = {
            'aws_bucket': self.aws_bucket,
            'aws_identity': 'identity',
            'aws_secret': 'credential',
            'aws_endpoint': 'http://swift.example.com:8080/auth/v1.0',
            'account': 'account',
            'container': 'container',
            'protocol': 'swift'
        }
        sync = SyncContainer(self.scratch_space, settings, max_conns=1)
        self.assertIsInstance(sync.provider, SyncSwift)
        self.assertEqual(sync.provider.settings, settings)
        self.assertEqual(len(sync.provider.client_pool.client_pool), 0)
        self.assertEqual(sync.provider.client_pool.pool_size, 1)

    def test_unknown_provider(self):
        settings = {
            'aws_bucket': self.aws_bucket,
            'aws_identity': 'identity',
            'aws_secret': 'credential',
            'account': 'account',
            'container': 'container',
            'protocol': 'foo'
        }
        with self.assertRaises(NotImplementedError):
            SyncContainer(self.scratch_space, settings, 1)

    @mock.patch('s3_sync.sync_s3.boto3.session.Session')
    def test_retry_copy_after(self, session_mock):
        settings = {
            'aws_bucket': self.aws_bucket,
            'aws_identity': 'identity',
            'aws_secret': 'credential',
            'account': 'account',
            'container': 'container',
            'copy_after': 3600
        }
        with self.assertRaises(RetryError):
            sync = SyncContainer(self.scratch_space, settings)
            sync.handle({'deleted': 0, 'created_at': str(time.time())}, None)

        current = time.time()
        with mock.patch('s3_sync.sync_container.time') as time_mock:
            time_mock.time.return_value = current + settings['copy_after'] + 1
            sync = SyncContainer(self.scratch_space, settings)
            sync.provider = mock.Mock()
            sync.handle(
                {
                    'deleted': 0,
                    'created_at': str(time.time()),
                    'name': 'foo',
                    'storage_policy_index': 99
                }, None)
            sync.provider.upload_object.assert_called_once_with(
                'foo', 99, None)

    @mock.patch('s3_sync.sync_s3.boto3.session.Session')
    def test_retain_copy(self, session_mock):
        settings = {
            'aws_bucket': self.aws_bucket,
            'aws_identity': 'identity',
            'aws_secret': 'credential',
            'account': 'account',
            'container': 'container',
            'retain_local': False
        }

        sync = SyncContainer(self.scratch_space, settings)
        sync.provider = mock.Mock()
        swift_client = mock.Mock()
        row = {
            'deleted': 0,
            'created_at': str(time.time() - 5),
            'name': 'foo',
            'storage_policy_index': 99
        }
        sync.handle(row, swift_client)

        _, _, swift_ts = decode_timestamps(row['created_at'])
        swift_ts.offset += 1

        sync.provider.upload_object.assert_called_once_with(
            row['name'], 99, swift_client)
        swift_client.delete_object.assert_called_once_with(
            settings['account'],
            settings['container'],
            row['name'],
            headers={'X-Timestamp': Timestamp(swift_ts).internal})

    @mock.patch('s3_sync.sync_s3.boto3.session.Session')
    def test_no_propagate_delete(self, session_mock):
        settings = {
            'aws_bucket': self.aws_bucket,
            'aws_identity': 'identity',
            'aws_secret': 'credential',
            'account': 'account',
            'container': 'container',
            'propagate_delete': False
        }

        sync = SyncContainer(self.scratch_space, settings)
        sync.provider = mock.Mock()
        row = {'deleted': 1, 'name': 'tombstone'}
        sync.handle(row, None)

        # Make sure we do nothing with this row
        self.assertEqual([], sync.provider.mock_calls)

    @mock.patch('s3_sync.sync_s3.boto3.session.Session')
    def test_propagate_delete(self, session_mock):
        settings = {
            'aws_bucket': self.aws_bucket,
            'aws_identity': 'identity',
            'aws_secret': 'credential',
            'account': 'account',
            'container': 'container',
            'propagate_delete': True
        }

        sync = SyncContainer(self.scratch_space, settings)
        sync.provider = mock.Mock()
        row = {'deleted': 1, 'name': 'tombstone'}
        sync.handle(row, None)

        # Make sure that we do not make any additional calls
        self.assertEqual([mock.call.delete_object(row['name'])],
                         sync.provider.mock_calls)
Esempio n. 15
0
class TestSyncContainer(unittest.TestCase):
    class MockMetaConf(object):
        def __init__(self, fake_status):
            self.fake_status = fake_status
            self.write_buf = ''

        def read(self, size=-1):
            if size != -1:
                raise RuntimeError()
            return json.dumps(self.fake_status)

        def write(self, data):
            # Only support write at the beginning
            self.write_buf += data

        def truncate(self, size=None):
            if size:
                raise RuntimeError('Not supported')
            self.fake_status = json.loads(self.write_buf)
            self.write_buf = ''

        def __exit__(self, *args):
            if self.write_buf:
                self.fake_status = json.loads(self.write_buf)
                self.write_buf = ''

        def __enter__(self):
            return self

        def seek(self, offset, flags=None):
            if offset != 0:
                raise RuntimeError

    @mock.patch('s3_sync.sync_s3.boto3.session.Session')
    def setUp(self, mock_boto3):
        self.mock_boto3_session = mock.Mock()
        self.mock_boto3_client = mock.Mock()

        mock_boto3.return_value = self.mock_boto3_session
        self.mock_boto3_session.client.return_value = self.mock_boto3_client

        self.aws_bucket = 'bucket'
        self.scratch_space = 'scratch'

        self.stats_factory = mock.Mock()
        stats_reporter = mock.Mock()
        self.stats_factory.instance.return_value = stats_reporter

        self.sync_container = SyncContainer(
            self.scratch_space, {
                'aws_bucket': self.aws_bucket,
                'aws_identity': 'identity',
                'aws_secret': 'credential',
                'account': 'account',
                'container': 'container'
            }, self.stats_factory)

    def test_load_non_existent_meta(self):
        ret = self.sync_container.get_last_processed_row('db-id')
        self.assertEqual(0, ret)

    @mock.patch('__builtin__.open')
    @mock.patch('s3_sync.sync_container.os.path.exists')
    def test_load_upgrade_status(self, mock_exists, mock_open):
        mock_exists.return_value = True
        fake_status = dict(last_row=42)
        mock_open.return_value = self.MockMetaConf(fake_status)

        status = self.sync_container.get_last_processed_row('db-id')
        self.assertEqual(fake_status['last_row'], status)

        mock_exists.assert_called_with(
            '%s/%s/%s' % (self.scratch_space, self.sync_container._account,
                          self.sync_container._container))

    @mock.patch('__builtin__.open')
    @mock.patch('s3_sync.sync_container.os.path.exists')
    def test_last_row_new_bucket(self, mock_exists, mock_open):
        db_id = 'db-id-test'
        new_bucket = 'new-bucket'
        self.sync_container.aws_bucket = 'bucket'
        fake_status = {db_id: dict(last_row=42, aws_bucket=new_bucket)}

        mock_exists.return_value = True
        mock_open.return_value = self.MockMetaConf(fake_status)

        status = self.sync_container.get_last_processed_row(db_id)
        self.assertEqual(0, status)

        mock_exists.assert_called_with(
            '%s/%s/%s' % (self.scratch_space, self.sync_container._account,
                          self.sync_container._container))

    @mock.patch('__builtin__.open')
    @mock.patch('s3_sync.sync_container.os.path.exists')
    def test_last_row_new_db_id(self, mock_exists, mock_open):
        db_id = 'db-id-test'
        self.sync_container.aws_bucket = 'bucket'
        fake_status = {db_id: dict(last_row=42, aws_bucket='bucket')}

        mock_exists.return_value = True
        mock_open.return_value = self.MockMetaConf(fake_status)

        status = self.sync_container.get_last_processed_row('other-db-id')
        self.assertEqual(0, status)

        mock_exists.assert_called_with(
            '%s/%s/%s' % (self.scratch_space, self.sync_container._account,
                          self.sync_container._container))

    @mock.patch('__builtin__.open')
    @mock.patch('s3_sync.sync_container.os.path.exists')
    def test_last_row_new_policy(self, mock_exists, mock_open):
        db_id = 'db-id-test'
        for field in SyncContainer.POLICY_FIELDS:
            if field != 'copy_after':
                setattr(self.sync_container, field, False)
            else:
                setattr(self.sync_container, field, 42)
        fake_status = {
            db_id:
            dict(last_row=42,
                 aws_bucket='bucket',
                 policy=dict(retain_local=True,
                             propagate_delete=True,
                             copy_after=0))
        }

        mock_exists.return_value = True
        mock_open.return_value = self.MockMetaConf(fake_status)

        status = self.sync_container.get_last_processed_row(db_id)
        self.assertEqual(0, status)

        mock_exists.assert_called_with(
            '%s/%s/%s' % (self.scratch_space, self.sync_container._account,
                          self.sync_container._container))

    @mock.patch('__builtin__.open')
    @mock.patch('s3_sync.sync_container.os.path.exists')
    def test_last_row_old_policy(self, mock_exists, mock_open):
        db_id = 'db-id-test'
        fake_status = {
            db_id:
            dict(last_row=42,
                 aws_bucket='bucket',
                 policy=dict(retain_local=True,
                             propagate_delete=True,
                             copy_after=0))
        }

        mock_exists.return_value = True
        mock_open.return_value = self.MockMetaConf(fake_status)

        status = self.sync_container.get_last_processed_row(db_id)
        self.assertEqual(42, status)

        mock_exists.assert_called_with(
            '%s/%s/%s' % (self.scratch_space, self.sync_container._account,
                          self.sync_container._container))

    @mock.patch('__builtin__.open')
    @mock.patch('s3_sync.sync_container.os.path.exists')
    def test_last_row(self, mock_exists, mock_open):
        db_entries = [{
            'id': 'db-id-1',
            'aws_bucket': 'bucket',
            'last_row': 5
        }, {
            'id': 'db-id-2',
            'aws_bucket': 'bucket',
            'last_row': 7
        }]
        for entry in db_entries:
            self.sync_container.aws_bucket = entry['aws_bucket']
            fake_status = {
                entry['id']:
                dict(last_row=entry['last_row'],
                     aws_bucket=entry['aws_bucket'])
            }

            mock_exists.return_value = True
            mock_open.return_value = self.MockMetaConf(fake_status)

            status = self.sync_container.get_last_processed_row(entry['id'])
            self.assertEqual(entry['last_row'], status)

            mock_exists.assert_called_with(
                '%s/%s/%s' % (self.scratch_space, self.sync_container._account,
                              self.sync_container._container))

    @mock.patch('__builtin__.open')
    @mock.patch('s3_sync.sync_container.os.path.exists')
    def test_get_last_verified_row(self, mock_exists, mock_open):
        db_entries = [{
            'id': 'db-id-1',
            'aws_bucket': 'bucket',
            'last_row': 5
        }, {
            'id': 'db-id-2',
            'aws_bucket': 'bucket',
            'last_verified_row': 7
        }, {
            'id': 'db-id-3',
            'aws_bucket': 'bucket',
            'last_row': 100,
            'last_verified_row': 10
        }]
        for entry in db_entries:
            self.sync_container.aws_bucket = entry['aws_bucket']
            fake_status = {entry['id']: dict(aws_bucket=entry['aws_bucket'])}
            if 'last_row' in entry:
                fake_status[entry['id']]['last_row'] = entry['last_row']
            if 'last_verified_row' in entry:
                fake_status[entry['id']]['last_verified_row'] =\
                    entry['last_verified_row']

            mock_exists.return_value = True
            mock_open.return_value = self.MockMetaConf(fake_status)

            status = self.sync_container.get_last_verified_row(entry['id'])
            if 'last_verified_row' in entry:
                self.assertEqual(entry['last_verified_row'], status)
            else:
                self.assertEqual(entry['last_row'], status)

            mock_exists.assert_called_with(
                '%s/%s/%s' % (self.scratch_space, self.sync_container._account,
                              self.sync_container._container))

    @mock.patch('__builtin__.open')
    def test_save_last_processed_row(self, mock_open):
        db_entries = {
            'db-id-1': {
                'aws_bucket': 'bucket',
                'last_row': 5
            },
            'db-id-2': {
                'aws_bucket': 'bucket',
                'last_row': 7
            }
        }
        new_row = 42
        for db_id, entry in db_entries.items():
            self.sync_container.aws_bucket = entry['aws_bucket']
            fake_conf_file = self.MockMetaConf(db_entries)
            mock_open.return_value = fake_conf_file

            with mock.patch('s3_sync.sync_container.os.path.exists')\
                    as mock_exists:
                mock_exists.return_value = True

                self.sync_container.save_last_processed_row(new_row, db_id)
                file_entries = fake_conf_file.fake_status
                for file_db_id, status in file_entries.items():
                    if file_db_id == db_id:
                        self.assertEqual(new_row, status['last_row'])
                        self.assertEqual(entry['last_row'],
                                         status['last_verified_row'])
                    else:
                        self.assertEqual(db_entries[file_db_id]['last_row'],
                                         status['last_row'])
                    if db_id != file_db_id:
                        continue
                    else:
                        self.assertIn('policy', status)
                    for field in SyncContainer.POLICY_FIELDS:
                        self.assertEqual(status['policy'][field],
                                         getattr(self.sync_container, field))

                self.assertEqual([
                    mock.call(
                        '%s/%s' %
                        (self.scratch_space, self.sync_container._account)),
                    mock.call(
                        '%s/%s/%s' %
                        (self.scratch_space, self.sync_container._account,
                         self.sync_container._container))
                ], mock_exists.call_args_list)

    @mock.patch('__builtin__.open')
    def test_save_last_verified_row(self, mock_open):
        db_entries = {
            'db-id-1': {
                'aws_bucket': 'bucket',
                'last_row': 100,
                'last_verified_row': 10
            },
            'db-id-2': {
                'aws_bucket': 'bucket',
                'last_row': 200,
                'last_verified_row': 50
            },
            'db-id-3': {
                'aws_bucket': 'bucket',
                'last_row': 1000
            }
        }
        new_row = 99
        for db_id, entry in db_entries.items():
            self.sync_container.aws_bucket = entry['aws_bucket']
            fake_conf_file = self.MockMetaConf(db_entries)
            mock_open.return_value = fake_conf_file

            with mock.patch('s3_sync.sync_container.os.path.exists')\
                    as mock_exists:
                mock_exists.return_value = True

                self.sync_container.save_last_verified_row(new_row, db_id)
                file_entries = fake_conf_file.fake_status
                for file_db_id, status in file_entries.items():
                    if file_db_id == db_id:
                        self.assertEqual(new_row, status['last_verified_row'])
                    elif 'last_verified_row' in db_entries[file_db_id]:
                        self.assertEqual(
                            db_entries[file_db_id]['last_verified_row'],
                            status['last_verified_row'])
                    self.assertEqual(db_entries[file_db_id]['last_row'],
                                     status['last_row'])
                    if db_id != file_db_id:
                        continue
                    else:
                        self.assertIn('policy', status)
                    for field in SyncContainer.POLICY_FIELDS:
                        self.assertEqual(status['policy'][field],
                                         getattr(self.sync_container, field))

                self.assertEqual([
                    mock.call(
                        '%s/%s' %
                        (self.scratch_space, self.sync_container._account)),
                    mock.call(
                        '%s/%s/%s' %
                        (self.scratch_space, self.sync_container._account,
                         self.sync_container._container))
                ], mock_exists.call_args_list)

    @mock.patch('__builtin__.open')
    @mock.patch('s3_sync.sync_container.os.path.exists')
    def test_save_no_prior_status(self, mock_exists, mock_open):
        def existence_check(path):
            if path == '%s/%s' % (self.scratch_space,
                                  self.sync_container._account):
                return True
            elif path == '%s/%s/%s' % (self.scratch_space,
                                       self.sync_container._account,
                                       self.sync_container._container):
                return False
            else:
                raise RuntimeError('Invalid path')

        self.sync_container.aws_bucket = 'bucket'
        fake_conf_file = self.MockMetaConf({})
        mock_exists.side_effect = existence_check
        mock_open.return_value = fake_conf_file

        self.sync_container.save_last_processed_row(42, 'db-id')
        self.assertEqual(42, fake_conf_file.fake_status['db-id']['last_row'])
        self.assertEqual(
            0, fake_conf_file.fake_status['db-id']['last_verified_row'])
        self.assertEqual('bucket',
                         fake_conf_file.fake_status['db-id']['aws_bucket'])

        self.assertEqual([
            mock.call('%s/%s' %
                      (self.scratch_space, self.sync_container._account)),
            mock.call('%s/%s/%s' %
                      (self.scratch_space, self.sync_container._account,
                       self.sync_container._container))
        ], mock_exists.call_args_list)

    @mock.patch('__builtin__.open')
    @mock.patch('s3_sync.sync_container.os.path.exists')
    def test_save_last_processed_row_new_bucket(self, mock_exists, mock_open):
        db_entries = {
            'db-id-1': {
                'aws_bucket': 'bucket',
                'last_row': 5
            },
            'db-id-2': {
                'aws_bucket': 'old-bucket',
                'last_row': 7
            }
        }
        new_row = 42
        for db_id, entry in db_entries.items():
            self.sync_container.aws_bucket = 'bucket'
            fake_conf_file = self.MockMetaConf(db_entries)
            mock_open.return_value = fake_conf_file

            with mock.patch('s3_sync.sync_container.os.path.exists')\
                    as mock_exists:
                mock_exists.return_value = True
                self.sync_container.save_last_processed_row(new_row, db_id)
                file_entries = fake_conf_file.fake_status
                for file_db_id, status in file_entries.items():
                    if file_db_id == db_id:
                        self.assertEqual(new_row, status['last_row'])
                        self.assertEqual('bucket', status['aws_bucket'])
                    else:
                        self.assertEqual(db_entries[file_db_id]['last_row'],
                                         status['last_row'])
                        self.assertEqual(db_entries[file_db_id]['aws_bucket'],
                                         status['aws_bucket'])

                self.assertEqual([
                    mock.call(
                        '%s/%s' %
                        (self.scratch_space, self.sync_container._account)),
                    mock.call(
                        '%s/%s/%s' %
                        (self.scratch_space, self.sync_container._account,
                         self.sync_container._container))
                ], mock_exists.call_args_list)

    def test_s3_provider(self):
        defaults = {
            'aws_bucket': self.aws_bucket,
            'aws_identity': 'identity',
            'aws_secret': 'credential',
            'account': 'account',
            'container': 'container'
        }
        test_settings = [
            defaults, dict(defaults.items() + [('protocol', 's3')])
        ]

        for settings in test_settings:
            sync = SyncContainer(self.scratch_space,
                                 settings,
                                 self.stats_factory,
                                 max_conns=1)
            self.assertIsInstance(sync.provider, SyncS3)
            self.assertEqual(sync.provider.settings, settings)
            self.assertEqual(len(sync.provider.client_pool.client_pool), 0)
            self.assertEqual(sync.provider.client_pool.pool_size, 1)

    def test_swift_provider(self):
        settings = {
            'aws_bucket': self.aws_bucket,
            'aws_identity': 'identity',
            'aws_secret': 'credential',
            'aws_endpoint': 'http://swift.example.com:8080/auth/v1.0',
            'account': 'account',
            'container': 'container',
            'protocol': 'swift'
        }
        sync = SyncContainer(self.scratch_space,
                             settings,
                             self.stats_factory,
                             max_conns=1)
        self.assertIsInstance(sync.provider, SyncSwift)
        self.assertEqual(sync.provider.settings, settings)
        self.assertEqual(len(sync.provider.client_pool.client_pool), 0)
        self.assertEqual(sync.provider.client_pool.pool_size, 1)

    def test_unknown_provider(self):
        settings = {
            'aws_bucket': self.aws_bucket,
            'aws_identity': 'identity',
            'aws_secret': 'credential',
            'account': 'account',
            'container': 'container',
            'protocol': 'foo'
        }
        with self.assertRaises(NotImplementedError):
            SyncContainer(self.scratch_space,
                          settings,
                          self.stats_factory,
                          max_conns=1)

    @mock.patch('s3_sync.sync_s3.boto3.session.Session')
    def test_retry_copy_after(self, session_mock):
        settings = {
            'aws_bucket': self.aws_bucket,
            'aws_identity': 'identity',
            'aws_secret': 'credential',
            'account': 'account',
            'container': 'container',
            'copy_after': 3600
        }
        with self.assertRaises(RetryError):
            sync = SyncContainer(self.scratch_space, settings,
                                 self.stats_factory)
            sync.handle(
                {
                    'deleted': 0,
                    'created_at': str(time.time()),
                    'name': 'foo'
                }, None)

        current = time.time()
        with mock.patch('s3_sync.sync_container.time') as time_mock:
            time_mock.time.return_value = current + settings['copy_after'] + 1
            sync = SyncContainer(self.scratch_space, settings,
                                 self.stats_factory)
            sync.provider = mock.Mock()
            row = {
                'deleted': 0,
                'created_at': str(time.time()),
                'name': 'foo',
                'storage_policy_index': 99
            }
            sync.handle(row, None)
            sync.provider.upload_object.assert_called_once_with(
                row, None, mock.ANY)
            sync.stats_reporter.increment.assert_not_called()

    @mock.patch('s3_sync.sync_s3.boto3.session.Session')
    def test_retain_copy(self, session_mock):
        settings = {
            'aws_bucket': self.aws_bucket,
            'aws_identity': 'identity',
            'aws_secret': 'credential',
            'account': 'account',
            'container': 'container',
            'retain_local': False
        }

        sync = SyncContainer(self.scratch_space, settings, self.stats_factory)
        sync.provider = mock.Mock()
        sync.provider.upload_object.return_value = SyncS3.UploadStatus.PUT
        swift_client = mock.Mock()
        swift_client.get_object_metadata.return_value = {}
        row = {
            'deleted': 0,
            'created_at': str(time.time() - 5),
            'name': 'foo',
            'storage_policy_index': 99
        }
        sync.handle(row, swift_client)

        _, _, swift_ts = decode_timestamps(row['created_at'])

        sync.provider.upload_object.assert_called_once_with(
            row, swift_client, mock.ANY)
        sync.provider.delete_local_object.assert_called_once_with(
            swift_client, row, swift_ts, False)
        sync.stats_reporter.increment.assert_called_once_with(
            'copied_objects', 1)

    @mock.patch('s3_sync.sync_s3.boto3.session.Session')
    def test_no_propagate_delete(self, session_mock):
        settings = {
            'aws_bucket': self.aws_bucket,
            'aws_identity': 'identity',
            'aws_secret': 'credential',
            'account': 'account',
            'container': 'container',
            'propagate_delete': False
        }

        sync = SyncContainer(self.scratch_space, settings, self.stats_factory)
        sync.provider = mock.Mock()
        row = {'deleted': 1, 'name': 'tombstone'}
        sync.handle(row, None)

        # Make sure we do nothing with this row
        self.assertEqual([], sync.provider.mock_calls)

    @mock.patch('s3_sync.sync_s3.boto3.session.Session')
    def test_propagate_delete(self, session_mock):
        settings = {
            'aws_bucket': self.aws_bucket,
            'aws_identity': 'identity',
            'aws_secret': 'credential',
            'account': 'account',
            'container': 'container',
            'propagate_delete': True
        }

        sync = SyncContainer(self.scratch_space, settings, self.stats_factory)
        sync.provider = mock.Mock()
        row = {'deleted': 1, 'name': 'tombstone'}
        sync.handle(row, None)

        # Make sure that we do not make any additional calls
        self.assertEqual([mock.call.delete_object(row['name'])],
                         sync.provider.mock_calls)
        sync.stats_reporter.increment.assert_called_once_with(
            'deleted_objects', 1)

    def test_hash_invariance(self):
        testdata = [({}, {}), ({
            'foo': 'bar'
        }, {
            'foo': 'bar'
        }), ({
            'foo1': 'bar1',
            'foo2': 'bar2'
        }, {
            'foo2': 'bar2',
            'foo1': 'bar1'
        }),
                    ({
                        '\xd8\xaafoo': '\xd8\xaabar',
                        'foo': 'bar'
                    }, {
                        'foo': 'bar',
                        '\xd8\xaafoo': '\xd8\xaabar'
                    })]
        for case in testdata:
            self.assertEqual(hash_dict(case[0]), hash_dict(case[1]))

    @mock.patch('__builtin__.open')
    @mock.patch('s3_sync.sync_container.os.path.exists')
    @mock.patch('s3_sync.sync_swift.swiftclient.client.Connection')
    def test_handle_container_new_metadata(self, mock_swift, mock_exists,
                                           mock_open):
        mock_exists.return_value = True
        db_entries = {'db-id-1': {'aws_bucket': 'bucket', 'last_row': 5}}
        fake_conf_file = self.MockMetaConf(db_entries)
        mock_open.return_value = fake_conf_file
        settings = {
            'aws_bucket': self.aws_bucket,
            'aws_identity': 'identity',
            'aws_secret': 'credential',
            'aws_endpoint': 'http://example.com',
            'account': 'account',
            'sync_container_metadata': True,
            'container': 'container',
            'protocol': 'swift'
        }
        metadata = {
            'X-Container-Meta-Foo': ('foo', '1545179217.201453'),
            'Some-Other-Key': ('val', '1545179217.201453'),
            'X-Versions-Locations': ('versions', '1545179217.201453'),
            'X-Container-Meta-Bar': ('bar', '1545179217.201453'),
        }

        mock_swift.return_value.post_container.return_value = {}

        sync = SyncContainer(self.scratch_space, settings, self.stats_factory)
        sync.handle_container_info({'id': 'db-id-1'}, metadata)

        mock_swift.assert_called_once_with(authurl='http://example.com',
                                           key='credential',
                                           user='******',
                                           os_options={},
                                           retries=3)
        self.assertEqual([
            mock.call.post_container(self.aws_bucket,
                                     headers={
                                         'X-Container-Meta-Foo': 'foo',
                                         'X-Container-Meta-Bar': 'bar'
                                     })
        ], mock_swift.return_value.mock_calls)

    @mock.patch('__builtin__.open')
    @mock.patch('s3_sync.sync_container.os.path.exists')
    @mock.patch('s3_sync.sync_swift.swiftclient.client.Connection')
    def test_handle_container_update_metadata(self, mock_swift, mock_exists,
                                              mock_open):
        mock_exists.return_value = True
        old_hash = hash_dict({'X-Container-Meta': 'foo'})
        db_entries = {
            'db-id-1': {
                'aws_bucket': 'bucket',
                'last_row': 5,
                SyncContainer.METADATA_HASH_KEY: old_hash
            }
        }
        fake_conf_file = self.MockMetaConf(db_entries)
        mock_open.return_value = fake_conf_file

        settings = {
            'aws_bucket': self.aws_bucket,
            'aws_identity': 'identity',
            'aws_secret': 'credential',
            'aws_endpoint': 'http://example.com',
            'account': 'account',
            'sync_container_metadata': True,
            'container': 'container',
            'protocol': 'swift'
        }

        sync = SyncContainer(self.scratch_space, settings, self.stats_factory)
        metadata = {
            'X-Container-Meta-Foo': ('foo', '1545160000.1234'),
            'X-Container-Meta-Bar': ('bar', '1546123456.7896'),
            'Some-Other-Key': ('val', '1545179217.201453'),
            'X-Versions-Locations': ('versions', '1545179217.201453')
        }
        mock_swift.return_value.post_container.return_value = {}

        sync.handle_container_info({'id': 'db-id-1'}, metadata)
        mock_swift.assert_called_once_with(authurl='http://example.com',
                                           key='credential',
                                           user='******',
                                           os_options={},
                                           retries=3)
        self.assertEqual([
            mock.call.post_container(self.aws_bucket,
                                     headers={
                                         'X-Container-Meta-Foo': 'foo',
                                         'X-Container-Meta-Bar': 'bar'
                                     })
        ], mock_swift.return_value.mock_calls)

    @mock.patch('__builtin__.open')
    @mock.patch('s3_sync.sync_container.os.path.exists')
    @mock.patch('s3_sync.sync_swift.swiftclient.client.Connection')
    def test_handle_container_same_metadata(self, mock_swift, mock_exists,
                                            mock_open):
        mock_exists.return_value = True
        metadata = {
            'X-Container-Meta-Foo': ('foo', '1545160000.1234'),
            'X-Container-Meta-Bar': ('bar', '1546123456.7896'),
            'Some-Other-Key': ('val', '1545179217.201453'),
            'X-Versions-Locations': ('versions', '1545179217.201453')
        }
        meta_hash = hash_dict({
            'X-Container-Meta-Foo': 'foo',
            'X-Container-Meta-Bar': 'bar'
        })
        db_entries = {
            'db-id-1': {
                'aws_bucket': 'bucket',
                'last_row': 5,
                SyncContainer.METADATA_HASH_KEY: meta_hash
            }
        }
        fake_conf_file = self.MockMetaConf(db_entries)
        mock_open.return_value = fake_conf_file

        settings = {
            'aws_bucket': self.aws_bucket,
            'aws_identity': 'identity',
            'aws_endpoint': 'http://example.com',
            'aws_secret': 'credential',
            'account': 'account',
            'sync_container_metadata': True,
            'container': 'container',
            'protocol': 'swift'
        }

        sync = SyncContainer(self.scratch_space, settings, self.stats_factory)

        sync.handle_container_info({'id': 'db-id-1'}, metadata)
        # Connections are lazy-initialized
        mock_swift.assert_not_called()

    @mock.patch('s3_sync.sync_swift.swiftclient.client.Connection')
    def test_handle_container_info_errors(self, mock_swift):
        db_entries = {'db-id-1': {'aws_bucket': 'bucket', 'last_row': 5}}
        settings = {
            'aws_bucket': self.aws_bucket,
            'aws_identity': 'identity',
            'aws_secret': 'credential',
            'aws_endpoint': 'http://example.com',
            'account': 'account',
            'sync_container_metadata': True,
            'container': 'container',
            'protocol': 'swift'
        }

        metadata = {
            'X-Container-Meta-Foo': ('foo', '1545179217.201453'),
            'X-Delete-At': ('1645179217', '1545179217.201453'),
            'Some-Other-Key': ('val', '1545179217.201453'),
            'X-Versions-Locations': ('versions', '1545179217.201453'),
            'X-Container-Meta-Bar': ('bar', '1545179217.201453'),
        }

        tmpdir = tempfile.mkdtemp()
        try:
            os.makedirs(os.path.join(tmpdir, 'account'))
            with open(os.path.join(tmpdir, 'account', 'container'), 'w') as fh:
                fh.write(json.dumps(db_entries))

            sync = SyncContainer(tmpdir, settings, self.stats_factory)
            sync.logger = mock.Mock()
            mock_swift.return_value.post_container.side_effect = RuntimeError(
                'failed to post container')
            sync.handle_container_info({'id': 'db-id-1'}, metadata)
            sync.logger.error.assert_called_once_with(mock.ANY)
            sync.logger.debug.assert_called_once_with(mock.ANY)
        finally:
            shutil.rmtree(tmpdir)

    def test_skips_matching_exclude_rows(self):
        settings = {
            'aws_bucket': self.aws_bucket,
            'aws_identity': 'identity',
            'aws_secret': 'credential',
            'account': 'account',
            'container': 'container',
            'exclude_pattern': '^foo-\d+$'
        }
        sync = SyncContainer(self.scratch_space, settings, self.stats_factory)
        sync.provider = mock.Mock()
        sync.handle({'name': 'foo-1'}, mock.Mock())
        sync.handle({'name': 'foo-12345', 'deleted': 1}, mock.Mock())
        sync.handle(
            {
                'name': 'foo-1-bar',
                'deleted': 0,
                'created_at': '123456789.1234'
            }, mock.Mock())
        self.assertEqual([
            mock.call.upload_object(
                {
                    'name': 'foo-1-bar',
                    'deleted': 0,
                    'created_at': '123456789.1234'
                }, mock.ANY, mock.ANY)
        ], sync.provider.mock_calls)