Skip to content

Commit

Permalink
Improve post-test checks on metadata
Browse files Browse the repository at this point in the history
Signed-off-by: mulhern <[email protected]>
  • Loading branch information
mulkieran committed Nov 22, 2024
1 parent e30395c commit 9d0a4a2
Show file tree
Hide file tree
Showing 2 changed files with 270 additions and 35 deletions.
268 changes: 268 additions & 0 deletions testlib/check_metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
#!/usr/bin/env python3

# Copyright 2021 Red Hat, Inc.
#
# 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.

"""
Inspect Stratis pool-level metadata and produce various kinds of output.
"""

# isort: STDLIB
import argparse
import json
import sys
from collections import defaultdict
from copy import deepcopy
from uuid import UUID

SIZE_OF_STRATIS_METADATA_SECTORS = 8192
SIZE_OF_CRYPT_METADATA_SECTORS = 32768


class Key: # pylint: disable=too-few-public-methods
"""
Keys in the pool-level metadata.
"""

ALLOCS = "allocs"
BACKSTORE = "backstore"
BLOCKDEV = "blockdev"
CAP = "cap"
CRYPT_META_ALLOCS = "crypt_meta_allocs"
DATA_TIER = "data_tier"
DEVS = "devs"
INTEGRITY_META_ALLOCS = "integrity_meta_allocs"
LENGTH = "length"
PARENT = "parent"
START = "start"
UUID = "uuid"


class MetadataError: # pylint: disable=too-few-public-methods
"""
Any error in the metadata.
"""

def __init__(self, msg):
self.msg = msg

def __str__(self):
return self.msg


class Skipped: # pylint: disable=too-few-public-methods
"""
Information about an unallocated extent.
"""

def __init__(self, start, length, prev_start, prev_length):
self.start = start
self.length = length
self.prev_start = prev_start
self.prev_length = prev_length

def __str__(self):
return f"({self.start}, {self.length}) after ({self.prev_start}, {self.prev_length})"


def _check_1(devs_map):
"""
Verify that integrity_meta_allocs, if existing, have length a multiple of
4 KiB.
:param devs_map: map of device UUID to other info
:type devs_map: dict of UUID * list
:return: list of MetadataError
"""
errors = []

for uuid, integrity_meta_allocs in devs_map.items():
for alloc in [] if integrity_meta_allocs is None else integrity_meta_allocs:
if alloc[1] % 8 != 0:
errors.append(
MetadataError(
f"integrity meta_allocs length {alloc[1]} sectors for "
f"device {uuid} is not a multiple of 4KiB"
)
)

return errors


def _check_2(allocs, *, init=0, skipped=None):
"""
Verify that no allocations overlap
:param allocs: list of extents
:type allocs: list of int * int
:param int init: initiall offset
:param skipped: use to return a list of skipped extents
:type skipped: list or NoneType
"""
errors = []

assert isinstance(allocs, list), "must be list to sort properly"

current_block = init
(prev_start, prev_length) = (0, init)
for start, length in sorted(allocs, key=lambda item: item[0]):

if start < current_block:
errors.append(
MetadataError(
f"allocation ({start, length}) overlaps with previous "
f"allocation which extends to {current_block}"
),
)
elif start > current_block:
if skipped is not None:
skipped.append(
Skipped(
current_block, start - current_block, prev_start, prev_length
)
)

current_block = start + length
(prev_start, prev_length) = (start, length)

return errors


def _check_3(crypt_meta_allocs):
"""
Verify a few basic things about the crypt meta allocs
"""
errors = []

if len(crypt_meta_allocs) != 1:
errors.append(
MetadataError("Multiple allocations for crypt meta allocs"),
)

# Get the one element.
crypt_meta_allocs = crypt_meta_allocs[0]

if crypt_meta_allocs[0] != 0:
errors.append(
MetadataError(
"crypt meta allocs offset from the start of the underlying "
f"device by {crypt_meta_allocs[0]}"
)
)

if crypt_meta_allocs[1] != SIZE_OF_CRYPT_METADATA_SECTORS:
errors.append(
MetadataError(
"crypt meta allocs length does not equal expected {SIZE_OF_CRYPT_METADATA_SECTORS}"
)
)

return errors


def check(metadata):
"""
Check pool-level metadata for consistency.
:param metadata: all the pool-level metadata.
:type metadata: Python JSON representation
:return: list of MetadataError
"""

errors = []

data_tier_devs = metadata[Key.BACKSTORE][Key.DATA_TIER][Key.BLOCKDEV][Key.DEVS]

data_tier_devs_map = dict(
(UUID(dev[Key.UUID]), dev.get(Key.INTEGRITY_META_ALLOCS))
for dev in data_tier_devs
)

errors.extend(_check_1(data_tier_devs_map))

data_tier_allocs = metadata[Key.BACKSTORE][Key.DATA_TIER][Key.BLOCKDEV][Key.ALLOCS][
0
]

# in case the device has been extended and more integrity metadata has
# been allocated, there may be more than one entry for a device.
data_tier_allocs_map = defaultdict(list)
for item in data_tier_allocs:
data_tier_allocs_map[UUID(item[Key.PARENT])].append(
[item[Key.START], item[Key.LENGTH]]
)

all_data_tier_allocs = deepcopy(data_tier_allocs_map)
for uuid, allocs in data_tier_devs_map.items():
all_data_tier_allocs[uuid].extend(allocs)

for uuid, allocs in all_data_tier_allocs.items():
skipped = []
errors.extend(
_check_2(allocs, init=SIZE_OF_STRATIS_METADATA_SECTORS, skipped=skipped)
)
if skipped:
print(
f"Skipped in device {uuid}: {', '.join(str(x) for x in skipped)}",
file=sys.stderr,
)

crypt_meta_allocs = metadata[Key.BACKSTORE][Key.CAP].get(Key.CRYPT_META_ALLOCS)

errors.extend(_check_3(crypt_meta_allocs))

cap_allocs = metadata[Key.BACKSTORE][Key.CAP].get(Key.ALLOCS)

skipped = []
errors.extend(
_check_2(cap_allocs, init=SIZE_OF_CRYPT_METADATA_SECTORS, skipped=skipped)
)
if skipped:
print(f"Skipped in cap: {', '.join(str(x) for x in skipped)}", file=sys.stderr)

return errors


def _gen_parser():
"""
Generate the parser.
"""
parser = argparse.ArgumentParser(
description=("Inspect Stratis pool-level metadata.")
)

parser.add_argument("file", help="The file with the pool-level metadata")
return parser


def main():
"""
The main method.
"""

parser = _gen_parser()

args = parser.parse_args()

with open(args.file, "r", encoding="utf-8") as infile:
metadata = json.load(infile)

errors = check(metadata)

if errors:
raise RuntimeError(errors)


if __name__ == "__main__":
main()
37 changes: 2 additions & 35 deletions testlib/infra.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import dbus
from justbytes import Range

from .check_metadata import check
from .dbus import StratisDbus, manager_interfaces
from .utils import exec_command, process_exists, terminate_traces

Expand Down Expand Up @@ -269,37 +270,6 @@ def _check_encryption_information_consistency(self, pool_object_path, metadata):
elif features is not None:
self.assertNotIn("Encryption", metadata["features"])

def _check_crypt_meta_allocs(self, metadata):
"""
Check that all crypt metadata allocs exist and have non-zero length.
"""
crypt_meta_allocs = metadata["backstore"]["cap"].get("crypt_meta_allocs")
self.assertIsNotNone(crypt_meta_allocs)
self.assertIsInstance(crypt_meta_allocs, list)
self.assertGreater(len(crypt_meta_allocs), 0)

crypt_meta_allocs = crypt_meta_allocs[0]
self.assertIsInstance(crypt_meta_allocs, list)
self.assertEqual(crypt_meta_allocs[0], 0)
self.assertGreater(crypt_meta_allocs[1], 0)

def _check_integrity_meta_allocs(self, metadata):
"""
Check that all integrity_meta_allocs exist and have non-zero length.
"""
for integrity_meta_allocs in [
a["integrity_meta_allocs"]
for a in metadata["backstore"]["data_tier"]["blockdev"]["devs"]
]:
self.assertIsNotNone(integrity_meta_allocs)
self.assertIsInstance(integrity_meta_allocs, list)
self.assertGreater(len(integrity_meta_allocs), 0)

for alloc in integrity_meta_allocs:
start, length = Range(alloc[0], 512), Range(alloc[1], 512)
self.assertGreater(start, Range(0))
self.assertEqual(length % Range(8, 512), Range(0))

def run_check(self, stop_time):
"""
Run the check.
Expand Down Expand Up @@ -331,10 +301,7 @@ def run_check(self, stop_time):
self._check_thin_meta_allocations(written)

self._check_encryption_information_consistency(object_path, written)
self._check_crypt_meta_allocs(written)

self._check_integrity_meta_allocs(written)

self.assertEqual(check(written), [])
else:
current_message = (
"" if current_return_code == _OK else current_message
Expand Down

0 comments on commit 9d0a4a2

Please sign in to comment.