Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add nvlist command to sdb #288

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion sdb/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ def _call(self, objs: Iterable[drgn.Object]) -> Iterable[drgn.Object]:
err_msg = f"invalid memory access: {str(err)}"
else:
err_msg = "invalid memory access while handling object "
err_msg += "at address {hex(obj.address_of_().value_())}"
err_msg += f"at address {hex(obj.address_of_().value_())}"
cmd_err = CommandError(self.name, err_msg)
print(cmd_err.text)
if result is not None:
Expand Down
26 changes: 26 additions & 0 deletions sdb/commands/nvpair/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#
# Copyright 2019 Delphix
#
# 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.
#

# pylint: disable=missing-docstring

import glob
import importlib
import os

for path in glob.glob("{}/*.py".format(os.path.dirname(__file__))):
if path != __file__:
module = os.path.splitext(os.path.basename(path))[0]
importlib.import_module("sdb.commands.nvpair.{}".format(module))
27 changes: 27 additions & 0 deletions sdb/commands/nvpair/internal/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#
# Copyright 2019 Delphix
#
# 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.
#

# pylint: disable=missing-docstring

import glob
import importlib
import os

for path in glob.glob("{}/*.py".format(os.path.dirname(__file__))):
if path != __file__:
module = os.path.splitext(os.path.basename(path))[0]
importlib.import_module(
"sdb.commands.nvpair.internal.{}".format(module))
210 changes: 210 additions & 0 deletions sdb/commands/nvpair/nvpair.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
#
# Copyright 2020 Datto, 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.
#

# pylint: disable=missing-docstring

import os
import drgn
import sdb


class Nvpair:
# Member data and methods to print the value of the
# various nvpair_t types
def __init__(self, nvp: drgn.Object, nvl: drgn.Object) -> None:
self.addr = nvp.address_
self.nvp = nvp
self.nvl = nvl
self.nvp_size_ = sdb.type_canonicalize_size("nvpair_t")
self.nvl_size = sdb.type_canonicalize_size("nvlist_t")
self.name_size = int(nvp.nvp_name_sz)
self.data_ptr = self.addr + ((self.nvp_size_ + self.name_size + 7) & ~7)

def get_type(self, strip: bool = False) -> str:
# N.B. data_type_t enum is not zero indexed it starts
# with -1 so we add one to get the correct index
fields = sdb.get_type("data_type_t").type.enumerators
enum_string: str = fields[self.nvp.nvp_type + 1].name
if strip:
prefix = os.path.commonprefix([f[0] for f in fields])
return enum_string[prefix.rfind("_") + 1:]
return enum_string

def get_name(self) -> drgn.Object:
# name is located after the nvpair_t struct
ptr = self.addr + self.nvp_size_
return sdb.create_object("char *", ptr).string_().decode("utf-8")

# works for all signed and unsigned int, long, long long
@staticmethod
def get_value_int(type_: str, loc: int) -> str:
sz = sdb.create_object('size_t', sdb.type_canonicalize_size(type_))
# value is located after the name
contents = sdb.get_prog().read(loc, sz)
value = drgn.Object.from_bytes_(sdb.get_prog(), type_, contents)
return str(value.value_())

# iterate the array, obtain and print nvpair values
def get_array_values(self, type_: str) -> None:
indent = " "
count = self.nvp.nvp_value_elem
type_ = type_.replace("_array", "")
if "boolean" in type_:
type_ = "uint32_t"
val = ""
# skip n 64 bit pointers for string and nvlist array type
offset = count * 8
for x in range(count):
if x == 0:
print("values=")
if "nvlist" in type_:
self.nvl.set_indent(self.nvl.indent + 4)
self.nvl.print_one(
sdb.create_object("nvlist_t *", self.data_ptr + offset))
self.nvl.set_indent(self.nvl.indent - 4)
offset += self.nvl_size
elif "int" in type_:
sz = sdb.create_object('size_t',
sdb.type_canonicalize_size(type_))
val = self.get_value_int(type_, self.data_ptr + (sz * x))
print(f"{indent}{val}")
elif "byte" in type_:
val = self.get_value_int("unsigned char", self.data_ptr + x)
print(f"{indent}{hex(int(val))}")
elif "string" in type_:
offset += len(val)
val = sdb.create_object("char *", self.data_ptr +
offset).string_().decode("utf-8")
print(f"{indent}{val}")
offset += 1 # null terminator

# obtain and print the nvpair value
def get_value(self) -> None:
type_ = (self.get_type(True) + "_t").lower()
if "array" in type_:
self.get_array_values(type_)
return
if "nvlist" in type_:
print("values=")
self.nvl.set_indent(self.nvl.indent + 4)
self.nvl.print_one(sdb.create_object("nvlist_t *", self.data_ptr))
self.nvl.set_indent(self.nvl.indent - 4)
return
print("value=", end='')
if "boolean_value" in type_:
type_ = "uint32_t"
elif "boolean" in type_: # DATA_TYPE_BOOLEAN has name and no value
print("")
elif "hrtime" in type_:
type_ = "int64_t"
if "int" in type_:
print(self.get_value_int(type_, self.data_ptr))
if "string" in type_:
print(
sdb.create_object("char *",
self.data_ptr).string_().decode("utf-8"))
if "byte" in type_:
print(hex(int(self.get_value_int("unsigned char", self.data_ptr))))
return


class Nvlist(sdb.SingleInputCommand):
"""
Print nvlist_t

DESCRIPTION
Print all the nvpair_t in the passed nvlist_t. Handle basic types,
array types, and nvlist_t types. Type double is omitted as it is
not used in zfs.

EXAMPLE
Print nvlist_t of snapshot holds from the nvlist_t * pointer address:

sdb> echo 0xffff970d0ff681e0 | nvlist
name=monday-1 type=DATA_TYPE_UINT64 value=1633989858
name=monday-2 type=DATA_TYPE_UINT64 value=1633989863
"""

names = ["nvlist"]
input_type = "nvlist_t *"
output_type = "nvlist_t *"
indent = 0

def set_indent(self, indent: int) -> None:
self.indent = indent

# nvlist iteration methods
@staticmethod
def nvlist_contains_nvp(nvl: drgn.Object, nvp: drgn.Object) -> int:
priv = drgn.cast("nvpriv_t *", nvl.nvl_priv)
if nvp.address_ == 0:
return 0

curr = priv.nvp_list
while not sdb.is_null(curr):
if curr.nvi_nvp.address_ == nvp.address_:
return 1
# pylint: disable=protected-access
curr = curr._nvi_un._nvi._nvi_next

return 0

@staticmethod
def nvlist_first_nvpair(nvl: drgn.Object) -> drgn.Object:
if sdb.is_null(nvl) or sdb.is_null(nvl.nvl_priv):
return None
priv = drgn.cast("nvpriv_t *", nvl.nvl_priv)
if sdb.is_null(priv.nvp_list):
return None
return priv.nvp_list.nvi_nvp
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It turns out that priv.nvp_list can be NULL, which is why the test output has the FaultError warning. Will fix.


@staticmethod
def nvlist_next_nvpair(nvl: drgn.Object, nvp: drgn.Object) -> drgn.Object:
if sdb.is_null(nvl) or sdb.is_null(nvl.nvl_priv):
return None

priv = drgn.cast("nvpriv_t *", nvl.nvl_priv)

curr_addr = nvp.address_ - drgn.offsetof(sdb.get_type("i_nvp_t"),
"nvi_nvp")
curr = sdb.create_object("i_nvp_t *", curr_addr)

if priv.nvp_curr == curr or Nvlist.nvlist_contains_nvp(nvl, nvp):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the nvlist_contains_nvp() check really necessary? Seems like it would make overall iteration O(N^2) since every call to next_nvpair() will iterate the whole nvlist. Maybe this isn't a big deal as long as your list isn't TOO long (<1000?)

Copy link
Contributor Author

@PaulZ-98 PaulZ-98 Jul 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. I basically ported
https://src.illumos.org/source/xref/illumos-gate/usr/src/common/nvpair/nvpair.c?r=b8a5bee1#1431
Let me do some experiments to see if or how often nvlist_contains_nvp is needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the extra check is there in case the nvlist is having element(s) removed while we're traversing it.

# pylint: disable=protected-access
curr = curr._nvi_un._nvi._nvi_next
else:
curr = drgn.NULL(sdb.get_prog(), "i_nvp_t *")

if not sdb.is_null(curr):
return curr.nvi_nvp

return None

# print one nvlist_t
def print_one(self, nvl: drgn.Object) -> None:
pair = self.nvlist_first_nvpair(nvl)
while pair is not None:
nvobj = Nvpair(pair, self)
print(f"{' '*self.indent}", end='')
print(f"name={nvobj.get_name()} ", end='')
print(f"type={nvobj.get_type()} ", end='')
# value will be printed in get_value function
nvobj.get_value()
pair = self.nvlist_next_nvpair(nvl, pair)

def _call_one(self, obj: drgn.Object) -> None:
self.print_one(obj)
print("----")
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"sdb.commands.internal",
"sdb.commands.linux",
"sdb.commands.linux.internal",
"sdb.commands.nvpair",
"sdb.commands.nvpair.internal",
"sdb.commands.spl",
"sdb.commands.spl.internal",
"sdb.commands.zfs",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name=enabled_feat type=DATA_TYPE_NVLIST values=
name=org.zfsonlinux:large_dnode type=DATA_TYPE_UINT64 value=0
name=com.delphix:redaction_bookmarks type=DATA_TYPE_UINT64 value=0
name=com.delphix:extensible_dataset type=DATA_TYPE_UINT64 value=1
name=com.delphix:bookmark_written type=DATA_TYPE_UINT64 value=0
name=com.delphix:hole_birth type=DATA_TYPE_UINT64 value=1
name=org.illumos:skein type=DATA_TYPE_UINT64 value=0
name=org.illumos:sha512 type=DATA_TYPE_UINT64 value=0
name=org.illumos:lz4_compress type=DATA_TYPE_UINT64 value=1
name=com.delphix:redacted_datasets type=DATA_TYPE_UINT64 value=0
name=com.datto:bookmark_v2 type=DATA_TYPE_UINT64 value=0
name=com.delphix:embedded_data type=DATA_TYPE_UINT64 value=1
name=com.delphix:device_removal type=DATA_TYPE_UINT64 value=0
name=org.open-zfs:large_blocks type=DATA_TYPE_UINT64 value=0
name=com.joyent:multi_vdev_crash_dump type=DATA_TYPE_UINT64 value=0
name=com.datto:encryption type=DATA_TYPE_UINT64 value=0
name=org.illumos:edonr type=DATA_TYPE_UINT64 value=0
name=com.delphix:spacemap_v2 type=DATA_TYPE_UINT64 value=1
name=com.joyent:filesystem_limits type=DATA_TYPE_UINT64 value=0
name=com.datto:resilver_defer type=DATA_TYPE_UINT64 value=0
name=com.delphix:spacemap_histogram type=DATA_TYPE_UINT64 value=24
name=com.delphix:async_destroy type=DATA_TYPE_UINT64 value=0
name=org.zfsonlinux:userobj_accounting type=DATA_TYPE_UINT64 value=1
name=com.delphix:zpool_checkpoint type=DATA_TYPE_UINT64 value=0
name=com.delphix:obsolete_counts type=DATA_TYPE_UINT64 value=0
name=com.delphix:empty_bpobj type=DATA_TYPE_UINT64 value=0
name=com.delphix:bookmarks type=DATA_TYPE_UINT64 value=0
name=com.delphix:enabled_txg type=DATA_TYPE_UINT64 value=26
name=com.delphix:log_spacemap type=DATA_TYPE_UINT64 value=1
name=org.zfsonlinux:allocation_classes type=DATA_TYPE_UINT64 value=0
name=org.zfsonlinux:project_quota type=DATA_TYPE_UINT64 value=1
name=com.delphix:livelist type=DATA_TYPE_UINT64 value=0
name=can_rdonly type=DATA_TYPE_BOOLEAN value=
name=rewind_txg_ts type=DATA_TYPE_UINT64 value=1575588901
name=seconds_of_rewind type=DATA_TYPE_INT64 value=-1575588901
name=verify_data_errors type=DATA_TYPE_UINT64 value=0
----
----
name=rewind_txg_ts type=DATA_TYPE_UINT64 value=1575589714
name=seconds_of_rewind type=DATA_TYPE_INT64 value=-1575589714
name=verify_data_errors type=DATA_TYPE_UINT64 value=3
----
3 changes: 2 additions & 1 deletion tests/integration/test_zfs_generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"dbuf | dbuf -l 1",
'dbuf | dbuf -l 1 | head | dbuf',

# spa + vdev + metaslab
# spa + vdev + metaslab + nvlist
"spa",
"spa -H",
"spa -v",
Expand All @@ -51,6 +51,7 @@
"spa | vdev | metaslab -w",
"spa | vdev | metaslab | member ms_allocatable | range_tree",
"spa | vdev | metaslab | member ms_allocatable.rt_root | zfs_btree",
"spa | member spa_load_info | nvlist",

# zfs_dbgmsg
"zfs_dbgmsg",
Expand Down