31
30
from eventlet import sleep, spawn, wsgi, listen, Timeout
32
31
from test.unit import FakeLogger
33
from test.unit import _setxattr as setxattr
34
32
from test.unit import connect_tcp, readuntil2crlfs
35
33
from swift.obj import server as object_server
34
from swift.obj import diskfile
36
35
from swift.common import utils
37
36
from swift.common.utils import hash_path, mkdirs, normalize_timestamp, \
38
NullLogger, storage_directory
39
from swift.common.exceptions import DiskFileNotExist
37
NullLogger, storage_directory, public, \
40
39
from swift.common import constraints
41
40
from eventlet import tpool
42
41
from swift.common.swob import Request, HeaderKeyDict
45
class TestDiskFile(unittest.TestCase):
46
"""Test swift.obj.server.DiskFile"""
49
""" Set up for testing swift.object_server.ObjectController """
50
self.testdir = os.path.join(mkdtemp(), 'tmp_test_obj_server_DiskFile')
51
mkdirs(os.path.join(self.testdir, 'sda1', 'tmp'))
53
self._real_tpool_execute = tpool.execute
54
def fake_exe(meth, *args, **kwargs):
55
return meth(*args, **kwargs)
56
tpool.execute = fake_exe
59
""" Tear down for testing swift.object_server.ObjectController """
60
rmtree(os.path.dirname(self.testdir))
61
tpool.execute = self._real_tpool_execute
63
def _create_test_file(self, data, keep_data_fp=True):
64
df = object_server.DiskFile(self.testdir, 'sda1', '0', 'a', 'c', 'o',
67
f = open(os.path.join(df.datadir,
68
normalize_timestamp(time()) + '.data'), 'wb')
70
setxattr(f.fileno(), object_server.METADATA_KEY,
71
pickle.dumps({}, object_server.PICKLE_PROTOCOL))
73
df = object_server.DiskFile(self.testdir, 'sda1', '0', 'a', 'c', 'o',
74
FakeLogger(), keep_data_fp=keep_data_fp)
77
def test_disk_file_app_iter_corners(self):
78
df = self._create_test_file('1234567890')
79
self.assertEquals(''.join(df.app_iter_range(0, None)), '1234567890')
81
df = object_server.DiskFile(self.testdir, 'sda1', '0', 'a', 'c', 'o',
82
FakeLogger(), keep_data_fp=True)
83
self.assertEqual(''.join(df.app_iter_range(5, None)), '67890')
85
def test_disk_file_app_iter_ranges(self):
86
df = self._create_test_file('012345678911234567892123456789')
87
it = df.app_iter_ranges([(0, 10), (10, 20), (20, 30)], 'plain/text',
88
'\r\n--someheader\r\n', 30)
90
self.assert_('0123456789' in value)
91
self.assert_('1123456789' in value)
92
self.assert_('2123456789' in value)
94
def test_disk_file_app_iter_ranges_edges(self):
95
df = self._create_test_file('012345678911234567892123456789')
96
it = df.app_iter_ranges([(3, 10), (0, 2)], 'application/whatever',
97
'\r\n--someheader\r\n', 30)
99
self.assert_('3456789' in value)
100
self.assert_('01' in value)
102
def test_disk_file_large_app_iter_ranges(self):
104
This test case is to make sure that the disk file app_iter_ranges
105
method all the paths being tested.
107
long_str = '01234567890' * 65536
108
target_strs = ['3456789', long_str[0:65590]]
109
df = self._create_test_file(long_str)
111
it = df.app_iter_ranges([(3, 10), (0, 65590)], 'plain/text',
112
'5e816ff8b8b8e9a5d355497e5d9e0301', 655360)
115
the produced string actually missing the MIME headers
116
need to add these headers to make it as real MIME message.
117
The body of the message is produced by method app_iter_ranges
118
off of DiskFile object.
120
header = ''.join(['Content-Type: multipart/byteranges;',
122
'5e816ff8b8b8e9a5d355497e5d9e0301\r\n'])
124
value = header + ''.join(it)
126
parts = map(lambda p: p.get_payload(decode=True),
127
email.message_from_string(value).walk())[1:3]
128
self.assertEqual(parts, target_strs)
130
def test_disk_file_app_iter_ranges_empty(self):
132
This test case tests when empty value passed into app_iter_ranges
133
When ranges passed into the method is either empty array or None,
134
this method will yield empty string
136
df = self._create_test_file('012345678911234567892123456789')
137
it = df.app_iter_ranges([], 'application/whatever',
138
'\r\n--someheader\r\n', 100)
139
self.assertEqual(''.join(it), '')
141
df = object_server.DiskFile(self.testdir, 'sda1', '0', 'a', 'c', 'o',
142
FakeLogger(), keep_data_fp=True)
143
it = df.app_iter_ranges(None, 'app/something',
144
'\r\n--someheader\r\n', 150)
145
self.assertEqual(''.join(it), '')
147
def test_disk_file_mkstemp_creates_dir(self):
148
tmpdir = os.path.join(self.testdir, 'sda1', 'tmp')
150
with object_server.DiskFile(self.testdir, 'sda1', '0', 'a', 'c',
151
'o', FakeLogger()).writer() as writer:
152
self.assert_(os.path.exists(tmpdir))
154
def test_iter_hook(self):
155
hook_call_count = [0]
157
hook_call_count[0] += 1
159
df = self._get_disk_file(fsize=65, csize=8, iter_hook=hook)
163
self.assertEquals(hook_call_count[0], 9)
165
def test_quarantine(self):
166
df = object_server.DiskFile(self.testdir, 'sda1', '0', 'a', 'c', 'o',
169
f = open(os.path.join(df.datadir,
170
normalize_timestamp(time()) + '.data'), 'wb')
171
setxattr(f.fileno(), object_server.METADATA_KEY,
172
pickle.dumps({}, object_server.PICKLE_PROTOCOL))
173
df = object_server.DiskFile(self.testdir, 'sda1', '0', 'a', 'c', 'o',
176
quar_dir = os.path.join(self.testdir, 'sda1', 'quarantined',
177
'objects', os.path.basename(os.path.dirname(
179
self.assert_(os.path.isdir(quar_dir))
181
def test_quarantine_same_file(self):
182
df = object_server.DiskFile(self.testdir, 'sda1', '0', 'a', 'c', 'o',
185
f = open(os.path.join(df.datadir,
186
normalize_timestamp(time()) + '.data'), 'wb')
187
setxattr(f.fileno(), object_server.METADATA_KEY,
188
pickle.dumps({}, object_server.PICKLE_PROTOCOL))
189
df = object_server.DiskFile(self.testdir, 'sda1', '0', 'a', 'c', 'o',
191
new_dir = df.quarantine()
192
quar_dir = os.path.join(self.testdir, 'sda1', 'quarantined',
193
'objects', os.path.basename(os.path.dirname(
195
self.assert_(os.path.isdir(quar_dir))
196
self.assertEquals(quar_dir, new_dir)
197
# have to remake the datadir and file
199
f = open(os.path.join(df.datadir,
200
normalize_timestamp(time()) + '.data'), 'wb')
201
setxattr(f.fileno(), object_server.METADATA_KEY,
202
pickle.dumps({}, object_server.PICKLE_PROTOCOL))
204
df = object_server.DiskFile(self.testdir, 'sda1', '0', 'a', 'c', 'o',
205
FakeLogger(), keep_data_fp=True)
206
double_uuid_path = df.quarantine()
207
self.assert_(os.path.isdir(double_uuid_path))
208
self.assert_('-' in os.path.basename(double_uuid_path))
210
def _get_disk_file(self, invalid_type=None, obj_name='o',
211
fsize=1024, csize=8, extension='.data', ts=None,
213
'''returns a DiskFile'''
214
df = object_server.DiskFile(self.testdir, 'sda1', '0', 'a', 'c',
215
obj_name, FakeLogger())
221
timestamp = str(normalize_timestamp(time()))
222
with df.writer() as writer:
225
etag = etag.hexdigest()
228
'X-Timestamp': timestamp,
229
'Content-Length': str(os.fstat(writer.fd).st_size),
231
writer.put(metadata, extension=extension)
232
if invalid_type == 'ETag':
234
etag.update('1' + '0' * (fsize - 1))
235
etag = etag.hexdigest()
236
metadata['ETag'] = etag
237
object_server.write_metadata(writer.fd, metadata)
238
if invalid_type == 'Content-Length':
239
metadata['Content-Length'] = fsize - 1
240
object_server.write_metadata(writer.fd, metadata)
242
df = object_server.DiskFile(self.testdir, 'sda1', '0', 'a', 'c',
243
obj_name, FakeLogger(),
244
keep_data_fp=True, disk_chunk_size=csize,
246
if invalid_type == 'Zero-Byte':
247
os.remove(df.data_file)
248
fp = open(df.data_file, 'w')
250
df.unit_test_len = fsize
253
def test_quarantine_valids(self):
254
df = self._get_disk_file(obj_name='1')
257
self.assertFalse(df.quarantined_dir)
259
df = self._get_disk_file(obj_name='2', csize=1)
262
self.assertFalse(df.quarantined_dir)
264
df = self._get_disk_file(obj_name='3', csize=100000)
267
self.assertFalse(df.quarantined_dir)
269
def run_quarantine_invalids(self, invalid_type):
270
df = self._get_disk_file(invalid_type=invalid_type, obj_name='1')
273
self.assertTrue(df.quarantined_dir)
274
df = self._get_disk_file(invalid_type=invalid_type,
275
obj_name='2', csize=1)
278
self.assertTrue(df.quarantined_dir)
279
df = self._get_disk_file(invalid_type=invalid_type,
280
obj_name='3', csize=100000)
283
self.assertTrue(df.quarantined_dir)
284
df = self._get_disk_file(invalid_type=invalid_type, obj_name='4')
285
self.assertFalse(df.quarantined_dir)
286
df = self._get_disk_file(invalid_type=invalid_type, obj_name='5')
287
for chunk in df.app_iter_range(0, df.unit_test_len):
289
self.assertTrue(df.quarantined_dir)
290
df = self._get_disk_file(invalid_type=invalid_type, obj_name='6')
291
for chunk in df.app_iter_range(0, df.unit_test_len + 100):
293
self.assertTrue(df.quarantined_dir)
294
expected_quar = False
295
# for the following, Content-Length/Zero-Byte errors will always result
296
# in a quarantine, even if the whole file isn't check-summed
297
if invalid_type in ('Zero-Byte', 'Content-Length'):
299
df = self._get_disk_file(invalid_type=invalid_type, obj_name='7')
300
for chunk in df.app_iter_range(1, df.unit_test_len):
302
self.assertEquals(bool(df.quarantined_dir), expected_quar)
303
df = self._get_disk_file(invalid_type=invalid_type, obj_name='8')
304
for chunk in df.app_iter_range(0, df.unit_test_len - 1):
306
self.assertEquals(bool(df.quarantined_dir), expected_quar)
307
df = self._get_disk_file(invalid_type=invalid_type, obj_name='8')
308
for chunk in df.app_iter_range(1, df.unit_test_len + 1):
310
self.assertEquals(bool(df.quarantined_dir), expected_quar)
312
def test_quarantine_invalids(self):
313
self.run_quarantine_invalids('ETag')
314
self.run_quarantine_invalids('Content-Length')
315
self.run_quarantine_invalids('Zero-Byte')
317
def test_quarantine_deleted_files(self):
318
df = self._get_disk_file(invalid_type='Content-Length',
321
self.assertTrue(df.quarantined_dir)
322
df = self._get_disk_file(invalid_type='Content-Length',
325
self.assertFalse(df.quarantined_dir)
326
df = self._get_disk_file(invalid_type='Content-Length',
328
self.assertRaises(DiskFileNotExist, df.get_data_file_size)
330
def test_put_metadata(self):
331
df = self._get_disk_file()
333
metadata = { 'X-Timestamp': ts, 'X-Object-Meta-test': 'data' }
334
df.put_metadata(metadata)
335
exp_name = '%s.meta' % str(normalize_timestamp(ts))
336
dl = os.listdir(df.datadir)
337
self.assertEquals(len(dl), 2)
338
self.assertTrue(exp_name in set(dl))
340
def test_put_metadata_ts(self):
341
df = self._get_disk_file()
343
metadata = { 'X-Timestamp': ts, 'X-Object-Meta-test': 'data' }
344
df.put_metadata(metadata, tombstone=True)
345
exp_name = '%s.ts' % str(normalize_timestamp(ts))
346
dl = os.listdir(df.datadir)
347
self.assertEquals(len(dl), 2)
348
self.assertTrue(exp_name in set(dl))
350
def test_unlinkold(self):
351
df1 = self._get_disk_file()
352
future_time = str(normalize_timestamp(time() + 100))
353
df2 = self._get_disk_file(ts=future_time)
354
self.assertEquals(len(os.listdir(df1.datadir)), 2)
355
df1.unlinkold(future_time)
356
self.assertEquals(len(os.listdir(df1.datadir)), 1)
357
self.assertEquals(os.listdir(df1.datadir)[0], "%s.data" % future_time)
359
def test_close_error(self):
362
raise Exception("bad")
364
df = self._get_disk_file(fsize=1024 * 1024 * 2)
365
df._handle_close_quarantine = err
368
# close is called at the end of the iterator
369
self.assertEquals(df.fp, None)
370
self.assertEquals(len(df.logger.log_dict['error']), 1)
372
def test_quarantine_twice(self):
373
df = self._get_disk_file(invalid_type='Content-Length',
375
self.assert_(os.path.isfile(df.data_file))
376
quar_dir = df.quarantine()
377
self.assertFalse(os.path.isfile(df.data_file))
378
self.assert_(os.path.isdir(quar_dir))
379
self.assertEquals(df.quarantine(), None)
382
44
class TestObjectController(unittest.TestCase):
383
45
""" Test swift.obj.server.ObjectController """
1361
1089
self.assertEquals(resp.status_int, 400)
1362
1090
# self.assertRaises(KeyError, self.object_controller.DELETE, req)
1364
timestamp = normalize_timestamp(time())
1365
req = Request.blank('/sda1/p/a/c/o',
1366
environ={'REQUEST_METHOD': 'DELETE'},
1367
headers={'X-Timestamp': timestamp})
1368
resp = self.object_controller.DELETE(req)
1369
self.assertEquals(resp.status_int, 404)
1372
timestamp = normalize_timestamp(time())
1373
req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'},
1375
'X-Timestamp': timestamp,
1376
'Content-Type': 'application/octet-stream',
1377
'Content-Length': '4',
1380
resp = self.object_controller.PUT(req)
1381
self.assertEquals(resp.status_int, 201)
1383
timestamp = normalize_timestamp(float(timestamp) - 1)
1384
req = Request.blank('/sda1/p/a/c/o',
1385
environ={'REQUEST_METHOD': 'DELETE'},
1386
headers={'X-Timestamp': timestamp})
1387
resp = self.object_controller.DELETE(req)
1388
self.assertEquals(resp.status_int, 204)
1389
objfile = os.path.join(self.testdir, 'sda1',
1390
storage_directory(object_server.DATADIR, 'p',
1391
hash_path('a', 'c', 'o')),
1393
self.assert_(os.path.isfile(objfile))
1396
timestamp = normalize_timestamp(time())
1397
req = Request.blank('/sda1/p/a/c/o',
1398
environ={'REQUEST_METHOD': 'DELETE'},
1399
headers={'X-Timestamp': timestamp})
1400
resp = self.object_controller.DELETE(req)
1401
self.assertEquals(resp.status_int, 204)
1402
objfile = os.path.join(self.testdir, 'sda1',
1403
storage_directory(object_server.DATADIR, 'p',
1404
hash_path('a', 'c', 'o')),
1406
self.assert_(os.path.isfile(objfile))
1092
# The following should have created a tombstone file
1093
timestamp = normalize_timestamp(time())
1094
req = Request.blank('/sda1/p/a/c/o',
1095
environ={'REQUEST_METHOD': 'DELETE'},
1096
headers={'X-Timestamp': timestamp})
1097
resp = self.object_controller.DELETE(req)
1098
self.assertEquals(resp.status_int, 404)
1099
objfile = os.path.join(self.testdir, 'sda1',
1100
storage_directory(object_server.DATADIR, 'p',
1101
hash_path('a', 'c', 'o')),
1103
self.assert_(os.path.isfile(objfile))
1105
# The following should *not* have created a tombstone file.
1106
timestamp = normalize_timestamp(float(timestamp) - 1)
1107
req = Request.blank('/sda1/p/a/c/o',
1108
environ={'REQUEST_METHOD': 'DELETE'},
1109
headers={'X-Timestamp': timestamp})
1110
resp = self.object_controller.DELETE(req)
1111
self.assertEquals(resp.status_int, 404)
1112
objfile = os.path.join(self.testdir, 'sda1',
1113
storage_directory(object_server.DATADIR, 'p',
1114
hash_path('a', 'c', 'o')),
1116
self.assertFalse(os.path.isfile(objfile))
1117
self.assertEquals(len(os.listdir(os.path.dirname(objfile))), 1)
1120
timestamp = normalize_timestamp(time())
1121
req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'},
1123
'X-Timestamp': timestamp,
1124
'Content-Type': 'application/octet-stream',
1125
'Content-Length': '4',
1128
resp = self.object_controller.PUT(req)
1129
self.assertEquals(resp.status_int, 201)
1131
# The following should *not* have created a tombstone file.
1132
timestamp = normalize_timestamp(float(timestamp) - 1)
1133
req = Request.blank('/sda1/p/a/c/o',
1134
environ={'REQUEST_METHOD': 'DELETE'},
1135
headers={'X-Timestamp': timestamp})
1136
resp = self.object_controller.DELETE(req)
1137
self.assertEquals(resp.status_int, 409)
1138
objfile = os.path.join(self.testdir, 'sda1',
1139
storage_directory(object_server.DATADIR, 'p',
1140
hash_path('a', 'c', 'o')),
1142
self.assertFalse(os.path.isfile(objfile))
1143
self.assertEquals(len(os.listdir(os.path.dirname(objfile))), 1)
1146
timestamp = normalize_timestamp(time())
1147
req = Request.blank('/sda1/p/a/c/o',
1148
environ={'REQUEST_METHOD': 'DELETE'},
1149
headers={'X-Timestamp': timestamp})
1150
resp = self.object_controller.DELETE(req)
1151
self.assertEquals(resp.status_int, 204)
1152
objfile = os.path.join(self.testdir, 'sda1',
1153
storage_directory(object_server.DATADIR, 'p',
1154
hash_path('a', 'c', 'o')),
1156
self.assert_(os.path.isfile(objfile))
1158
def test_DELETE_container_updates(self):
1159
# Test swift.object_server.ObjectController.DELETE and container
1160
# updates, making sure container update is called in the correct
1162
timestamp = normalize_timestamp(time())
1163
req = Request.blank('/sda1/p/a/c/o', environ={'REQUEST_METHOD': 'PUT'},
1165
'X-Timestamp': timestamp,
1166
'Content-Type': 'application/octet-stream',
1167
'Content-Length': '4',
1170
resp = self.object_controller.PUT(req)
1171
self.assertEquals(resp.status_int, 201)
1175
def our_container_update(*args, **kwargs):
1178
orig_cu = self.object_controller.container_update
1179
self.object_controller.container_update = our_container_update
1181
# The following request should return 409 (HTTP Conflict). A
1182
# tombstone file should not have been created with this timestamp.
1183
timestamp = normalize_timestamp(float(timestamp) - 1)
1184
req = Request.blank('/sda1/p/a/c/o',
1185
environ={'REQUEST_METHOD': 'DELETE'},
1186
headers={'X-Timestamp': timestamp})
1187
resp = self.object_controller.DELETE(req)
1188
self.assertEquals(resp.status_int, 409)
1189
objfile = os.path.join(self.testdir, 'sda1',
1190
storage_directory(object_server.DATADIR, 'p',
1191
hash_path('a', 'c', 'o')),
1193
self.assertFalse(os.path.isfile(objfile))
1194
self.assertEquals(len(os.listdir(os.path.dirname(objfile))), 1)
1195
self.assertEquals(0, calls_made[0])
1197
# The following request should return 204, and the object should
1198
# be truly deleted (container update is performed) because this
1199
# timestamp is newer. A tombstone file should have been created
1200
# with this timestamp.
1202
timestamp = normalize_timestamp(time())
1203
req = Request.blank('/sda1/p/a/c/o',
1204
environ={'REQUEST_METHOD': 'DELETE'},
1205
headers={'X-Timestamp': timestamp})
1206
resp = self.object_controller.DELETE(req)
1207
self.assertEquals(resp.status_int, 204)
1208
objfile = os.path.join(self.testdir, 'sda1',
1209
storage_directory(object_server.DATADIR, 'p',
1210
hash_path('a', 'c', 'o')),
1212
self.assert_(os.path.isfile(objfile))
1213
self.assertEquals(1, calls_made[0])
1214
self.assertEquals(len(os.listdir(os.path.dirname(objfile))), 1)
1216
# The following request should return a 404, as the object should
1217
# already have been deleted, but it should have also performed a
1218
# container update because the timestamp is newer, and a tombstone
1219
# file should also exist with this timestamp.
1221
timestamp = normalize_timestamp(time())
1222
req = Request.blank('/sda1/p/a/c/o',
1223
environ={'REQUEST_METHOD': 'DELETE'},
1224
headers={'X-Timestamp': timestamp})
1225
resp = self.object_controller.DELETE(req)
1226
self.assertEquals(resp.status_int, 404)
1227
objfile = os.path.join(self.testdir, 'sda1',
1228
storage_directory(object_server.DATADIR, 'p',
1229
hash_path('a', 'c', 'o')),
1231
self.assert_(os.path.isfile(objfile))
1232
self.assertEquals(2, calls_made[0])
1233
self.assertEquals(len(os.listdir(os.path.dirname(objfile))), 1)
1235
# The following request should return a 404, as the object should
1236
# already have been deleted, and it should not have performed a
1237
# container update because the timestamp is older, or created a
1238
# tombstone file with this timestamp.
1239
timestamp = normalize_timestamp(float(timestamp) - 1)
1240
req = Request.blank('/sda1/p/a/c/o',
1241
environ={'REQUEST_METHOD': 'DELETE'},
1242
headers={'X-Timestamp': timestamp})
1243
resp = self.object_controller.DELETE(req)
1244
self.assertEquals(resp.status_int, 404)
1245
objfile = os.path.join(self.testdir, 'sda1',
1246
storage_directory(object_server.DATADIR, 'p',
1247
hash_path('a', 'c', 'o')),
1249
self.assertFalse(os.path.isfile(objfile))
1250
self.assertEquals(2, calls_made[0])
1251
self.assertEquals(len(os.listdir(os.path.dirname(objfile))), 1)
1253
self.object_controller.container_update = orig_cu
1408
1255
def test_call(self):
1409
1256
""" Test swift.object_server.ObjectController.__call__ """