add byteorder information and logic to ring files

On-disk, serialized ring files are byteorder dependent, which makes then
unportable between different endian architectures. Add a field to the
ring dictionary in the file indicating the byteorder used to generate
the file, and then byteswap if necessary when deserializing the file.

This patch only allows newly created ring files to be byteoder
agnostic. Previously generated ring files will still fail on different
endian architectures, and will need to be regenerated with this patch.

Change-Id: I23b5e0a8082b30ca257aeb1fab03ab74e6f0b2d4
Closes-Bug: #1639980
This commit is contained in:
Drew Balfour 2016-11-08 14:17:31 -08:00
parent a30351bdfc
commit 1ec6e2bb0a
3 changed files with 120 additions and 46 deletions

View File

@ -26,6 +26,7 @@ from io import BufferedReader
from hashlib import md5
from itertools import chain
from tempfile import NamedTemporaryFile
import sys
from six.moves import range
@ -70,10 +71,15 @@ class RingData(object):
if metadata_only:
return ring_dict
byteswap = (ring_dict.get('byteorder', sys.byteorder) != sys.byteorder)
partition_count = 1 << (32 - ring_dict['part_shift'])
for x in range(ring_dict['replica_count']):
ring_dict['replica2part2dev_id'].append(
array.array('H', gz_file.read(2 * partition_count)))
part2dev = array.array('H', gz_file.read(2 * partition_count))
if byteswap:
part2dev.byteswap()
ring_dict['replica2part2dev_id'].append(part2dev)
return ring_dict
@classmethod
@ -117,7 +123,8 @@ class RingData(object):
json_encoder = json.JSONEncoder(sort_keys=True)
json_text = json_encoder.encode(
{'devs': ring['devs'], 'part_shift': ring['part_shift'],
'replica_count': len(ring['replica2part2dev_id'])})
'replica_count': len(ring['replica2part2dev_id']),
'byteorder': sys.byteorder})
json_len = len(json_text)
file_obj.write(struct.pack('!I', json_len))
file_obj.write(json_text)

View File

@ -21,6 +21,7 @@ from posix import stat_result, statvfs_result
from shutil import rmtree
import unittest
from unittest import TestCase
import sys
from swift import __version__ as swiftver
from swift.common import ring, utils
@ -313,23 +314,33 @@ class TestReconSuccess(TestCase):
def test_get_ring_md5(self):
# We should only see configured and present rings, so to handle the
# "normal" case just patch the policies to match the existing rings.
expt_out = {'%s/account.ring.gz' % self.tempdir:
'11e0c98abb209474d40d6a9a8a523803',
'%s/container.ring.gz' % self.tempdir:
'6685496a4045ce0be123068e0165a64d',
'%s/object.ring.gz' % self.tempdir:
'782728be98644fb725e165d4bf5728d4',
'%s/object-1.ring.gz' % self.tempdir:
'7c3a4bc9f724d4eb69c9b797cdc28b8c',
'%s/object-2.ring.gz' % self.tempdir:
'324b9c4da20cf7ef097edbd219d296e0'}
expt_out = {'little': {'%s/account.ring.gz' % self.tempdir:
'672c6c50dfcb77e04f5a2124aef87596',
'%s/container.ring.gz' % self.tempdir:
'4c4392f8bf816596990ca7cd4d4b6e50',
'%s/object.ring.gz' % self.tempdir:
'a34178f7399706e41395eb3ac8b2c4f3',
'%s/object-1.ring.gz' % self.tempdir:
'cd54c473676ce5e3103e68f0e9f2326d',
'%s/object-2.ring.gz' % self.tempdir:
'8783ec76f29bbcfd3f51acc63b2fc337'},
'big': {'%s/account.ring.gz' % self.tempdir:
'70d2dde8144c09e5b42858e0fa17ab7e',
'%s/container.ring.gz' % self.tempdir:
'0bd14f4327cbea88bde7ab7850d4d77d',
'%s/object.ring.gz' % self.tempdir:
'2d17555687af36fd8c7ee5b0c492c582',
'%s/object-1.ring.gz' % self.tempdir:
'bc1145d31771d7957d939077fe40e2e8',
'%s/object-2.ring.gz' % self.tempdir:
'cd95a01ae1ab158f2e9e4c207aeb1769'}}
# We need to instantiate app after overriding the configured policies.
# object-{1,2}.ring.gz should both appear as they are present on disk
# and were configured as policies.
app = recon.ReconMiddleware(FakeApp(), {'swift_dir': self.tempdir})
self.assertEqual(sorted(app.get_ring_md5().items()),
sorted(expt_out.items()))
sorted(expt_out[sys.byteorder].items()))
def test_get_ring_md5_ioerror_produces_none_hash(self):
# Ring files that are present but produce an IOError on read should
@ -369,13 +380,18 @@ class TestReconSuccess(TestCase):
raise IOError
return open(fn, fmode)
expt_out = {'%s/account.ring.gz' % self.tempdir: None,
'%s/container.ring.gz' % self.tempdir: None,
'%s/object.ring.gz' % self.tempdir:
'782728be98644fb725e165d4bf5728d4'}
expt_out = {'little': {'%s/account.ring.gz' % self.tempdir: None,
'%s/container.ring.gz' % self.tempdir: None,
'%s/object.ring.gz' % self.tempdir:
'a34178f7399706e41395eb3ac8b2c4f3'},
'big': {'%s/account.ring.gz' % self.tempdir: None,
'%s/container.ring.gz' % self.tempdir: None,
'%s/object.ring.gz' % self.tempdir:
'2d17555687af36fd8c7ee5b0c492c582'}}
ringmd5 = self.app.get_ring_md5(openr=fake_open_objonly)
self.assertEqual(sorted(ringmd5.items()),
sorted(expt_out.items()))
sorted(expt_out[sys.byteorder].items()))
@patch_policies([
StoragePolicy(0, 'stagecoach'),
@ -386,14 +402,22 @@ class TestReconSuccess(TestCase):
# If a configured ring is missing when the app is instantiated, but is
# later moved into place, we shouldn't need to restart object-server
# for it to appear in recon.
expt_out = {'%s/account.ring.gz' % self.tempdir:
'11e0c98abb209474d40d6a9a8a523803',
'%s/container.ring.gz' % self.tempdir:
'6685496a4045ce0be123068e0165a64d',
'%s/object.ring.gz' % self.tempdir:
'782728be98644fb725e165d4bf5728d4',
'%s/object-2.ring.gz' % self.tempdir:
'324b9c4da20cf7ef097edbd219d296e0'}
expt_out = {'little': {'%s/account.ring.gz' % self.tempdir:
'672c6c50dfcb77e04f5a2124aef87596',
'%s/container.ring.gz' % self.tempdir:
'4c4392f8bf816596990ca7cd4d4b6e50',
'%s/object.ring.gz' % self.tempdir:
'a34178f7399706e41395eb3ac8b2c4f3',
'%s/object-2.ring.gz' % self.tempdir:
'8783ec76f29bbcfd3f51acc63b2fc337'},
'big': {'%s/account.ring.gz' % self.tempdir:
'70d2dde8144c09e5b42858e0fa17ab7e',
'%s/container.ring.gz' % self.tempdir:
'0bd14f4327cbea88bde7ab7850d4d77d',
'%s/object.ring.gz' % self.tempdir:
'2d17555687af36fd8c7ee5b0c492c582',
'%s/object-2.ring.gz' % self.tempdir:
'cd95a01ae1ab158f2e9e4c207aeb1769'}}
# We need to instantiate app after overriding the configured policies.
# object-1.ring.gz should not appear as it's present but unconfigured.
@ -401,7 +425,7 @@ class TestReconSuccess(TestCase):
# present.
app = recon.ReconMiddleware(FakeApp(), {'swift_dir': self.tempdir})
self.assertEqual(sorted(app.get_ring_md5().items()),
sorted(expt_out.items()))
sorted(expt_out[sys.byteorder].items()))
# Simulate the configured policy's missing ringfile being moved into
# place during runtime
@ -412,12 +436,14 @@ class TestReconSuccess(TestCase):
array.array('H', [1, 1, 0, 3])]
self._create_ring(os.path.join(self.tempdir, ringfn),
ringmap, self.ring_devs, self.ring_part_shift)
expt_out[ringpath] = 'a7e591642beea6933f64aebd56f357d9'
expt_out[sys.byteorder][ringpath] = \
'77f752964f3bd4719e1b9b6cee47659f' if sys.byteorder == 'little' \
else '3e189e6c668a4d159707438dfb87589a'
# We should now see it in the ringmd5 response, without a restart
# (using the same app instance)
self.assertEqual(sorted(app.get_ring_md5().items()),
sorted(expt_out.items()))
sorted(expt_out[sys.byteorder].items()))
@patch_policies([
StoragePolicy(0, 'stagecoach', is_default=True),
@ -427,14 +453,22 @@ class TestReconSuccess(TestCase):
def test_get_ring_md5_excludes_configured_missing_obj_rings(self):
# Object rings that are configured but missing aren't meant to appear
# in the ringmd5 response.
expt_out = {'%s/account.ring.gz' % self.tempdir:
'11e0c98abb209474d40d6a9a8a523803',
'%s/container.ring.gz' % self.tempdir:
'6685496a4045ce0be123068e0165a64d',
'%s/object.ring.gz' % self.tempdir:
'782728be98644fb725e165d4bf5728d4',
'%s/object-2.ring.gz' % self.tempdir:
'324b9c4da20cf7ef097edbd219d296e0'}
expt_out = {'little': {'%s/account.ring.gz' % self.tempdir:
'672c6c50dfcb77e04f5a2124aef87596',
'%s/container.ring.gz' % self.tempdir:
'4c4392f8bf816596990ca7cd4d4b6e50',
'%s/object.ring.gz' % self.tempdir:
'a34178f7399706e41395eb3ac8b2c4f3',
'%s/object-2.ring.gz' % self.tempdir:
'8783ec76f29bbcfd3f51acc63b2fc337'},
'big': {'%s/account.ring.gz' % self.tempdir:
'70d2dde8144c09e5b42858e0fa17ab7e',
'%s/container.ring.gz' % self.tempdir:
'0bd14f4327cbea88bde7ab7850d4d77d',
'%s/object.ring.gz' % self.tempdir:
'2d17555687af36fd8c7ee5b0c492c582',
'%s/object-2.ring.gz' % self.tempdir:
'cd95a01ae1ab158f2e9e4c207aeb1769'}}
# We need to instantiate app after overriding the configured policies.
# object-1.ring.gz should not appear as it's present but unconfigured.
@ -442,7 +476,7 @@ class TestReconSuccess(TestCase):
# present.
app = recon.ReconMiddleware(FakeApp(), {'swift_dir': self.tempdir})
self.assertEqual(sorted(app.get_ring_md5().items()),
sorted(expt_out.items()))
sorted(expt_out[sys.byteorder].items()))
@patch_policies([
StoragePolicy(0, 'zero', is_default=True),
@ -450,19 +484,25 @@ class TestReconSuccess(TestCase):
def test_get_ring_md5_excludes_unconfigured_present_obj_rings(self):
# Object rings that are present but not configured in swift.conf
# aren't meant to appear in the ringmd5 response.
expt_out = {'%s/account.ring.gz' % self.tempdir:
'11e0c98abb209474d40d6a9a8a523803',
'%s/container.ring.gz' % self.tempdir:
'6685496a4045ce0be123068e0165a64d',
'%s/object.ring.gz' % self.tempdir:
'782728be98644fb725e165d4bf5728d4'}
expt_out = {'little': {'%s/account.ring.gz' % self.tempdir:
'672c6c50dfcb77e04f5a2124aef87596',
'%s/container.ring.gz' % self.tempdir:
'4c4392f8bf816596990ca7cd4d4b6e50',
'%s/object.ring.gz' % self.tempdir:
'a34178f7399706e41395eb3ac8b2c4f3'},
'big': {'%s/account.ring.gz' % self.tempdir:
'70d2dde8144c09e5b42858e0fa17ab7e',
'%s/container.ring.gz' % self.tempdir:
'0bd14f4327cbea88bde7ab7850d4d77d',
'%s/object.ring.gz' % self.tempdir:
'2d17555687af36fd8c7ee5b0c492c582'}}
# We need to instantiate app after overriding the configured policies.
# object-{1,2}.ring.gz should not appear as they are present on disk
# but were not configured as policies.
app = recon.ReconMiddleware(FakeApp(), {'swift_dir': self.tempdir})
self.assertEqual(sorted(app.get_ring_md5().items()),
sorted(expt_out.items()))
sorted(expt_out[sys.byteorder].items()))
def test_from_recon_cache(self):
oart = OpenAndReadTester(['{"notneeded": 5, "testkey1": "canhazio"}'])

View File

@ -24,6 +24,9 @@ from tempfile import mkdtemp
from shutil import rmtree
from time import sleep, time
import random
import sys
import copy
import mock
from six.moves import range
@ -107,6 +110,30 @@ class TestRingData(unittest.TestCase):
rd2 = ring.RingData.load(ring_fname)
self.assert_ring_data_equal(rd, rd2)
def test_byteswapped_serialization(self):
# Manually byte swap a ring and write it out, claiming it was written
# on a different endian machine. Then read it back in and see if it's
# the same as the non-byte swapped original.
ring_fname = os.path.join(self.testdir, 'foo.ring.gz')
data = [array.array('H', [0, 1, 0, 1]), array.array('H', [0, 1, 0, 1])]
swapped_data = copy.deepcopy(data)
for x in swapped_data:
x.byteswap()
with mock.patch.object(sys, 'byteorder',
'big' if sys.byteorder == 'little'
else 'little'):
rds = ring.RingData(swapped_data,
[{'id': 0, 'zone': 0}, {'id': 1, 'zone': 1}],
30)
rds.save(ring_fname)
rd1 = ring.RingData(data, [{'id': 0, 'zone': 0}, {'id': 1, 'zone': 1}],
30)
rd2 = ring.RingData.load(ring_fname)
self.assert_ring_data_equal(rd1, rd2)
def test_deterministic_serialization(self):
"""
Two identical rings should produce identical .gz files on disk.