summaryrefslogtreecommitdiff
path: root/trove/tests/fakes/swift.py
blob: acc1a46bfd2b757ff1dc1168e7a068eb0b547ce9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
# All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

from http import client as http_client
from hashlib import md5
from unittest.mock import MagicMock, patch
import json
import os
import socket
import swiftclient
import swiftclient.client as swift_client
import uuid

from oslo_log import log as logging
from swiftclient import client as swift


LOG = logging.getLogger(__name__)


class FakeSwiftClient(object):
    """Logs calls instead of executing."""
    def __init__(self, *args, **kwargs):
        pass

    @classmethod
    def Connection(cls, *args, **kargs):
        LOG.debug("fake FakeSwiftClient Connection")
        return FakeSwiftConnection()


class FakeSwiftConnection(object):
    """Logging calls instead of executing."""
    MANIFEST_QUERY_STRING_PUT = 'multipart-manifest=put'
    MANIFEST_QUERY_STRING_DELETE = 'multipart-manifest=delete'
    COPY_OBJECT_HEADER_KEY = 'X-Copy-From'
    url = 'http://mockswift/v1'

    def __init__(self, *args, **kwargs):
        self.manifest_prefix = None
        self.manifest_name = None
        self.container_objects = {}

    def get_auth(self):
        return (
            u"http://127.0.0.1:8080/v1/AUTH_c7b038976df24d96bf1980f5da17bd89",
            u'MIINrwYJKoZIhvcNAQcCoIINoDCCDZwCAQExCTAHBgUrDgMCGjCCDIgGCSqGSIb3'
            u'DQEHAaCCDHkEggx1eyJhY2Nlc3MiOiB7InRva2VuIjogeyJpc3N1ZWRfYXQiOiAi'
            u'MjAxMy0wMy0xOFQxODoxMzoyMC41OTMyNzYiLCAiZXhwaXJlcyI6ICIyMDEzLTAz'
            u'LTE5VDE4OjEzOjIwWiIsICJpZCI6ICJwbGFjZWhvbGRlciIsICJ0ZW5hbnQiOiB7'
            u'ImVuYWJsZWQiOiB0cnVlLCAiZGVzY3JpcHRpb24iOiBudWxsLCAibmFtZSI6ICJy'
            u'ZWRkd2FyZiIsICJpZCI6ICJjN2IwMzg5NzZkZjI0ZDk2YmYxOTgwZjVkYTE3YmQ4'
            u'OSJ9fSwgInNlcnZpY2VDYXRhbG9nIjogW3siZW5kcG9pbnRzIjogW3siYWRtaW5')

    def get_account(self):
        return ({'content-length': '2', 'accept-ranges': 'bytes',
                 'x-timestamp': '1363049003.92304',
                 'x-trans-id': 'tx9e5da02c49ed496395008309c8032a53',
                 'date': 'Tue, 10 Mar 2013 00:43:23 GMT',
                 'x-account-bytes-used': '0',
                 'x-account-container-count': '0',
                 'content-type': 'application/json; charset=utf-8',
                 'x-account-object-count': '0'}, [])

    def head_container(self, container):
        LOG.debug("fake head_container(%s)", container)
        if container == 'missing_container':
            raise swift.ClientException('fake exception',
                                        http_status=http_client.NOT_FOUND)
        elif container == 'unauthorized_container':
            raise swift.ClientException('fake exception',
                                        http_status=http_client.UNAUTHORIZED)
        elif container == 'socket_error_on_head':
            raise socket.error(111, 'ECONNREFUSED')
        pass

    def put_container(self, container):
        LOG.debug("fake put_container(%s)", container)
        pass

    def get_container(self, container, **kwargs):
        LOG.debug("fake get_container(%s)", container)
        fake_header = None
        fake_body = [{'name': 'backup_001'},
                     {'name': 'backup_002'},
                     {'name': 'backup_003'}]
        return fake_header, fake_body

    def head_object(self, container, name):
        LOG.debug("fake put_container(%(container)s, %(name)s)",
                  {'container': container, 'name': name})
        checksum = md5()
        if self.manifest_name == name:
            for object_name in sorted(self.container_objects):
                object_checksum = md5(self.container_objects[object_name])
                # The manifest file etag for a HEAD or GET is the checksum of
                # the concatenated checksums.
                checksum.update(object_checksum.hexdigest().encode())
            # this is included to test bad swift segment etags
            if name.startswith("bad_manifest_etag_"):
                return {'etag': '"this_is_an_intentional_bad_manifest_etag"'}
        else:
            if name in self.container_objects:
                checksum.update(self.container_objects[name])
            else:
                return {'etag': 'fake-md5-sum'}

        # Currently a swift HEAD object returns etag with double quotes
        return {'etag': '"%s"' % checksum.hexdigest()}

    def get_object(self, container, name, resp_chunk_size=None):
        LOG.debug("fake get_object(%(container)s, %(name)s)",
                  {'container': container, 'name': name})
        if container == 'socket_error_on_get':
            raise socket.error(111, 'ECONNREFUSED')
        if 'metadata' in name:
            fake_object_header = None
            metadata = {}
            if container == 'unsupported_version':
                metadata['version'] = '9.9.9'
            else:
                metadata['version'] = '1.0.0'
            metadata['backup_id'] = 123
            metadata['volume_id'] = 123
            metadata['backup_name'] = 'fake backup'
            metadata['backup_description'] = 'fake backup description'
            metadata['created_at'] = '2013-02-19 11:20:54,805'
            metadata['objects'] = [{
                'backup_001': {'compression': 'zlib', 'length': 10},
                'backup_002': {'compression': 'zlib', 'length': 10},
                'backup_003': {'compression': 'zlib', 'length': 10}
            }]
            metadata_json = json.dumps(metadata, sort_keys=True, indent=2)
            fake_object_body = metadata_json
            return (fake_object_header, fake_object_body)

        fake_header = {'etag': '"fake-md5-sum"'}
        if resp_chunk_size:
            def _object_info():
                length = 0
                while length < (1024 * 1024):
                    yield os.urandom(resp_chunk_size)
                    length += resp_chunk_size
            fake_object_body = _object_info()
        else:
            fake_object_body = os.urandom(1024 * 1024)
        return (fake_header, fake_object_body)

    def put_object(self, container, name, contents, **kwargs):
        LOG.debug("fake put_object(%(container)s, %(name)s)",
                  {'container': container, 'name': name})
        if container == 'socket_error_on_put':
            raise socket.error(111, 'ECONNREFUSED')
        headers = kwargs.get('headers', {})
        query_string = kwargs.get('query_string', '')
        object_checksum = md5()
        if query_string == self.MANIFEST_QUERY_STRING_PUT:
            # the manifest prefix format is <container>/<prefix> where
            # container is where the object segments are in and prefix is the
            # common prefix for all segments.
            self.manifest_name = name
            if isinstance(contents, str):
                object_checksum.update(contents.encode('utf-8'))
            else:
                object_checksum.update(contents)
        elif self.COPY_OBJECT_HEADER_KEY in headers:
            # this is a copy object operation
            source_path = headers.get(self.COPY_OBJECT_HEADER_KEY)
            source_name = source_path.split('/')[1]
            self.container_objects[name] = self.container_objects[source_name]
        else:
            if hasattr(contents, 'read'):
                chunk_size = 128
                object_content = b""
                chunk = contents.read(chunk_size)
                while chunk:
                    object_content += chunk
                    object_checksum.update(chunk)
                    chunk = contents.read(chunk_size)

                self.container_objects[name] = object_content
            else:
                object_checksum.update(contents)
                self.container_objects[name] = contents

            # this is included to test bad swift segment etags
            if name.startswith("bad_segment_etag_"):
                return "this_is_an_intentional_bad_segment_etag"
        return object_checksum.hexdigest()

    def post_object(self, container, name, headers={}):
        LOG.debug("fake post_object(%(container)s, %(name)s, %(head)s)",
                  {'container': container, 'name': name, 'head': str(headers)})

    def delete_object(self, container, name):
        LOG.debug("fake delete_object(%(container)s, %(name)s)",
                  {'container': container, 'name': name})
        if container == 'socket_error_on_delete':
            raise socket.error(111, 'ECONNREFUSED')
        pass


class Patcher(object):
    """Objects that need to mock global symbols throughout their existence
    should extend this base class.
    The object acts as a context manager which, when used in conjunction with
    the 'with' statement, terminates all running patchers when it leaves the
    scope.
    """

    def __init__(self):
        self.__patchers = None

    def __enter__(self):
        self.__patchers = []
        return self

    def __exit__(self, type, value, traceback):
        # Stop patchers in the LIFO order.
        while self.__patchers:
            self.__patchers.pop().stop()

    def _start_patcher(self, patcher):
        """All patchers started by this method will be automatically
        terminated on __exit__().
        """
        self.__patchers.append(patcher)
        return patcher.start()


class SwiftClientStub(Patcher):
    """
    Component for controlling behavior of Swift Client Stub.  Instantiated
    before tests are invoked in "fake" mode.  Invoke methods to control
    behavior so that systems under test can interact with this as it is a
    real swift client with a real backend

    example:

    if FAKE:
        swift_stub = SwiftClientStub()
        swift_stub.with_account('xyz')

    # returns swift account info and auth token
    component_using_swift.get_swift_account()

    if FAKE:
        swift_stub.with_container('test-container-name')

    # returns swift container information - mostly faked
    component_using.swift.create_container('test-container-name')
    component_using_swift.get_container_info('test-container-name')

    if FAKE:
        swift_stub.with_object('test-container-name', 'test-object-name',
            'test-object-contents')

    # returns swift object info and contents
    component_using_swift.create_object('test-container-name',
        'test-object-name', 'test-contents')
    component_using_swift.get_object('test-container-name', 'test-object-name')

    if FAKE:
        swift_stub.without_object('test-container-name', 'test-object-name')

    # allows object to be removed ONCE
    component_using_swift.remove_object('test-container-name',
        'test-object-name')
    # throws ClientException - 404
    component_using_swift.get_object('test-container-name', 'test-object-name')
    component_using_swift.remove_object('test-container-name',
        'test-object-name')

    if FAKE:
        swift_stub.without_object('test-container-name', 'test-object-name')

    # allows container to be removed ONCE
    component_using_swift.remove_container('test-container-name')
    # throws ClientException - 404
    component_using_swift.get_container('test-container-name')
    component_using_swift.remove_container('test-container-name')
    """

    def __init__(self):
        super(SwiftClientStub, self).__init__()
        self._connection = swift_client.Connection()
        self._containers = {}
        self._containers_list = []
        self._objects = {}

    def _remove_object(self, name, some_list):
        idx = [i for i, obj in enumerate(some_list) if obj['name'] == name]
        if len(idx) == 1:
            del some_list[idx[0]]

    def _ensure_object_exists(self, container, name):
        self._connection.get_object(container, name)

    def with_account(self, account_id):
        """
        setups up account headers

        example:

        if FAKE:
            swift_stub = SwiftClientStub()
            swift_stub.with_account('xyz')

        # returns swift account info and auth token
        component_using_swift.get_swift_account()

        :param account_id: account id
        """

        def account_resp():
            return ({'content-length': '2', 'accept-ranges': 'bytes',
                     'x-timestamp': '1363049003.92304',
                     'x-trans-id': 'tx9e5da02c49ed496395008309c8032a53',
                     'date': 'Tue, 10 Mar 2013 00:43:23 GMT',
                     'x-account-bytes-used': '0',
                     'x-account-container-count': '0',
                     'content-type': 'application/json; charset=utf-8',
                     'x-account-object-count': '0'}, self._containers_list)

        get_auth_return_value = (
            u"http://127.0.0.1:8080/v1/AUTH_c7b038976df24d96bf1980f5da17bd89",
            u'MIINrwYJKoZIhvcNAQcCoIINoDCCDZwCAQExCTAHBgUrDgMCGjCCDIgGCSqGSIb3'
            u'DQEHAaCCDHkEggx1eyJhY2Nlc3MiOiB7InRva2VuIjogeyJpc3N1ZWRfYXQiOiAi'
            u'MjAxMy0wMy0xOFQxODoxMzoyMC41OTMyNzYiLCAiZXhwaXJlcyI6ICIyMDEzLTAz'
            u'LTE5VDE4OjEzOjIwWiIsICJpZCI6ICJwbGFjZWhvbGRlciIsICJ0ZW5hbnQiOiB7'
            u'ImVuYWJsZWQiOiB0cnVlLCAiZGVzY3JpcHRpb24iOiBudWxsLCAibmFtZSI6ICJy'
            u'ZWRkd2FyZiIsICJpZCI6ICJjN2IwMzg5NzZkZjI0ZDk2YmYxOTgwZjVkYTE3YmQ4'
            u'OSJ9fSwgInNlcnZpY2VDYXRhbG9nIjogW3siZW5kcG9pbnRzIjogW3siYWRtaW5')

        get_auth_patcher = patch.object(
            swift_client.Connection, 'get_auth',
            MagicMock(return_value=get_auth_return_value))
        self._start_patcher(get_auth_patcher)

        get_account_patcher = patch.object(
            swift_client.Connection, 'get_account',
            MagicMock(return_value=account_resp()))
        self._start_patcher(get_account_patcher)

        return self

    def _create_container(self, container_name):
        container = {'count': 0, 'bytes': 0, 'name': container_name}
        self._containers[container_name] = container
        self._containers_list.append(container)
        self._objects[container_name] = []

    def _ensure_container_exists(self, container):
        self._connection.get_container(container)

    def _delete_container(self, container):
        self._remove_object(container, self._containers_list)
        del self._containers[container]
        del self._objects[container]

    def with_container(self, container_name):
        """
        sets expectations for creating a container and subsequently getting its
        information

        example:

        if FAKE:
            swift_stub.with_container('test-container-name')

        # returns swift container information - mostly faked
        component_using.swift.create_container('test-container-name')
        component_using_swift.get_container_info('test-container-name')

        :param container_name: container name that is expected to be created
        """

        def container_resp(container):
            return ({'content-length': '2', 'x-container-object-count': '0',
                     'accept-ranges': 'bytes', 'x-container-bytes-used': '0',
                     'x-timestamp': '1363370869.72356',
                     'x-trans-id': 'tx7731801ac6ec4e5f8f7da61cde46bed7',
                     'date': 'Fri, 10 Mar 2013 18:07:58 GMT',
                     'content-type': 'application/json; charset=utf-8'},
                    self._objects[container])

        # if this is called multiple times then nothing happens
        put_container_patcher = patch.object(swift_client.Connection,
                                             'put_container')
        self._start_patcher(put_container_patcher)

        def side_effect_func(*args, **kwargs):
            if args[0] in self._containers:
                return container_resp(args[0])
            else:
                raise swiftclient.ClientException('Resource Not Found',
                                                  http_status=404)

        self._create_container(container_name)
        # return container headers
        get_container_patcher = patch.object(
            swift_client.Connection, 'get_container',
            MagicMock(side_effect=side_effect_func))
        self._start_patcher(get_container_patcher)

        return self

    def without_container(self, container):
        """
        sets expectations for removing a container and subsequently throwing an
        exception for further interactions

        example:

        if FAKE:
            swift_stub.without_container('test-container-name')

        # returns swift container information - mostly faked
        component_using.swift.remove_container('test-container-name')
        # throws exception "Resource Not Found - 404"
        component_using_swift.get_container_info('test-container-name')

        :param container: container name that is expected to be removed
        """
        # first ensure container
        self._ensure_container_exists(container)
        self._delete_container(container)
        return self

    def with_object(self, container, name, contents):
        """
        sets expectations for creating an object and subsequently getting its
        contents

        example:

        if FAKE:
        swift_stub.with_object('test-container-name', 'test-object-name',
            'test-object-contents')

        # returns swift object info and contents
        component_using_swift.create_object('test-container-name',
            'test-object-name', 'test-contents')
        component_using_swift.get_object('test-container-name',
            'test-object-name')

        :param container: container name that is the object belongs
        :param name: the name of the object expected to be created
        :param contents: the contents of the object
        """

        put_object_patcher = patch.object(
            swift_client.Connection, 'put_object',
            MagicMock(return_value=uuid.uuid1()))
        self._start_patcher(put_object_patcher)

        def side_effect_func(*args, **kwargs):
            if (args[0] in self._containers and
                args[1] in map(lambda x: x['name'],
                               self._objects[args[0]])):
                return (
                    {'content-length': len(contents), 'accept-ranges': 'bytes',
                     'last-modified': 'Mon, 10 Mar 2013 01:06:34 GMT',
                     'etag': 'eb15a6874ce265e2c3eb1b4891567bab',
                     'x-timestamp': '1363568794.67584',
                     'x-trans-id': 'txef3aaf26c897420c8e77c9750ce6a501',
                     'date': 'Mon, 10 Mar 2013 05:35:14 GMT',
                     'content-type': 'application/octet-stream'},
                    [obj for obj in self._objects[args[0]]
                     if obj['name'] == args[1]][0]['contents'])
            else:
                raise swiftclient.ClientException('Resource Not Found',
                                                  http_status=404)

        get_object_patcher = patch.object(
            swift_client.Connection, 'get_object',
            MagicMock(side_effect=side_effect_func))
        self._start_patcher(get_object_patcher)

        self._remove_object(name, self._objects[container])
        self._objects[container].append(
            {'bytes': 13, 'last_modified': '2013-03-15T22:10:49.361950',
             'hash': 'ccc55aefbf92aa66f42b638802c5e7f6', 'name': name,
             'content_type': 'application/octet-stream', 'contents': contents})
        return self

    def without_object(self, container, name):
        """
        sets expectations for deleting an object

        example:

        if FAKE:
        swift_stub.without_object('test-container-name', 'test-object-name')

        # allows container to be removed ONCE
        component_using_swift.remove_container('test-container-name')
        # throws ClientException - 404
        component_using_swift.get_container('test-container-name')
        component_using_swift.remove_container('test-container-name')

        :param container: container name that is the object belongs
        :param name: the name of the object expected to be removed
        """
        self._ensure_container_exists(container)
        self._ensure_object_exists(container, name)

        def side_effect_func(*args, **kwargs):
            if not [obj for obj in self._objects[args[0]]
                    if obj['name'] == [args[1]]]:
                raise swiftclient.ClientException('Resource Not found',
                                                  http_status=404)
            else:
                return None

        delete_object_patcher = patch.object(
            swift_client.Connection, 'delete_object',
            MagicMock(side_effect=side_effect_func))
        self._start_patcher(delete_object_patcher)

        self._remove_object(name, self._objects[container])
        return self


def fake_create_swift_client(calculate_etag=False, *args):
    return FakeSwiftClient.Connection(*args)