-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Improve post-test checks on metadata
Signed-off-by: mulhern <[email protected]>
- Loading branch information
Showing
2 changed files
with
270 additions
and
35 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters