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 -kvm and -vcpu options to show information about VMs and VCPUs #130

Open
wants to merge 1 commit into
base: main
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
221 changes: 220 additions & 1 deletion drgn_tools/virtutil.py
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,206 @@
Utilities for virtualization
"""
import argparse
from typing import Iterable

from drgn import cast
from drgn import Object
from drgn import Program
from drgn.helpers.linux.cpumask import for_each_possible_cpu
from drgn.helpers.linux.list import list_for_each_entry
from drgn.helpers.linux.percpu import per_cpu
from drgn.helpers.linux.pid import find_task
from drgn.helpers.linux.sched import task_state_to_char
from drgn.helpers.linux.xarray import xa_for_each

from drgn_tools.corelens import CorelensModule
from drgn_tools.table import print_table
from drgn_tools.util import has_member


KVM_VCPU_STATE = {
0: "RUNNABLE",
1: "UNINITIALIZED",
2: "INIT_RECEIVED",
3: "HALTED",
4: "SIPI_RECEIVED",
5: "STOPPED",
6: "CHECK_STOP",
7: "OPERATING",
8: "LOAD",
}
Comment on lines +24 to +34
Copy link
Member

Choose a reason for hiding this comment

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

I see that these are defined in the code as #define. Since they're userspace API, they should not change, so it's totally fine to hardcode them. However, could you make this an enum? Also, maybe to help readers understand, it would be good to mention that in a comment that these have the prefix KVM_MP_STATE_ in the Linux code?

Suggested change
KVM_VCPU_STATE = {
0: "RUNNABLE",
1: "UNINITIALIZED",
2: "INIT_RECEIVED",
3: "HALTED",
4: "SIPI_RECEIVED",
5: "STOPPED",
6: "CHECK_STOP",
7: "OPERATING",
8: "LOAD",
}
enum KvmVcpuState:
"""Defined in include/uapi/linux/kvm.h, with prefix KVM_MP_STATE_"""
RUNNABLE = 0
UNINITIALIZED = 1
INIT_RECEIVED = 2
HALTED = 3
SIPI_RECEIVED = 4
STOPPED = 5
CHECK_STOP = 6
OPERATING = 7
LOAD = 8

When you need to look up the name by from an integer value, you can do KvmVcpuState(value).name, rather than KVM_VCPU_STATE[value].



def get_vm_list(prog: Program) -> Iterable[Object]:
"""
Returns list of Object of type ``struct kvm *
"""
vm_list = []
try:
vm_list = list(
list_for_each_entry(
"struct kvm", prog["vm_list"].address_of_(), "vm_list"
)
)
except KeyError:
print("No VMs running")

return vm_list
Comment on lines +37 to +51
Copy link
Member

Choose a reason for hiding this comment

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

How about naming this for_each_vm() and having it return Iterator[Object].

Suggested change
def get_vm_list(prog: Program) -> Iterable[Object]:
"""
Returns list of Object of type ``struct kvm *
"""
vm_list = []
try:
vm_list = list(
list_for_each_entry(
"struct kvm", prog["vm_list"].address_of_(), "vm_list"
)
)
except KeyError:
print("No VMs running")
return vm_list
def for_each_vm(prog: Program) -> Iterable[Object]:
"""
Iterates over all ``struct kvm *
"""
try:
yield from list_for_each_entry(
"struct kvm", prog["vm_list"].address_of_(), "vm_list"
)
except KeyError:
print("No VMs running")



def print_vm_list(prog: Program) -> None:
"""
Print information for all the VMs
"""
vm_list = get_vm_list(prog)

print(" =============<< VM LIST >>=================")
print("\n")
rows = [
[
"KVM",
"ONL",
"CRT",
"MEMSLOT_0",
"MEMSLOT_1",
"VCPUS",
"KVM_ARCH",
"KVM_STAT",
"PID",
"TASK",
"CPU",
"ST",
]
]

for vm in vm_list:
kvm_addr = hex(vm.value_())
pid = vm.userspace_pid.value_()
task = find_task(prog, pid)
if has_member(task, "cpu"):
cpu = task.cpu.value_()
else:
cpu = task.recent_used_cpu.value_()

if has_member(vm, "vcpus"):
vcpu = hex(vm.vcpus.address_of_())
else:
vcpu = hex(vm.vcpu_array.address_of_())

rows.append(
[
kvm_addr,
vm.online_vcpus.counter.value_(),
vm.created_vcpus.value_(),
hex(vm.memslots[0].value_()),
hex(vm.memslots[1].value_()),
vcpu,
hex(vm.arch.address_of_()),
hex(vm.stat.address_of_()),
pid,
hex(task.value_()),
cpu,
task_state_to_char(task),
]
)
print_table(rows)


def print_vcpu_list(prog: Program) -> None:
"""
Print information for all the vcpus of each VM
"""
vm_list = get_vm_list(prog)
print(" =============<< VCPU LIST >>=================")
print("\n")
rows = [
[
"KVM",
"VCPU",
"ID",
"IDX",
"ARCH",
"STAT",
"STAT_ID",
"STATE",
"CPU",
"TASK",
]
]

# for UEK5 to UEK7:
for vm in vm_list:
if has_member(vm, "vcpus"):
struct_vcpu = vm.vcpus
for i in range(len(struct_vcpu)):
if hex(struct_vcpu[i].value_()) == "0x0":
break
vcpu = hex(struct_vcpu[i].value_())
id = struct_vcpu[i].vcpu_id.value_()
if has_member(struct_vcpu[i], "vcpu_idx"):
idx = struct_vcpu[i].vcpu_idx.value_()
else:
idx = i
arch = hex(struct_vcpu[i].arch.address_of_())
stat = hex(struct_vcpu[i].stat.address_of_())
if has_member(struct_vcpu[i], "stats_id"):
stat_id = struct_vcpu[i].stats_id.string_().decode("utf-8")
else:
# The UEK5 does not have kvm_vcpu.stats_id structure member. Print None instead.
stat_id = None
state = struct_vcpu[i].arch.mp_state.value_()
cpu = struct_vcpu[i].cpu.value_()
if has_member(struct_vcpu[i], "wait"):
task = hex(struct_vcpu[i].wait.task.value_())
else:
# The UEK5 does not have kvm_vcpu.wait structure member. Print None instead.
task = None
rows.append(
[
hex(vm.value_()),
vcpu,
id,
idx,
arch,
stat,
stat_id,
KVM_VCPU_STATE[state],
cpu,
str(task),
]
)
# For UEK-NEXT:
else:
for _, entry in xa_for_each(vm.vcpu_array.address_of_()):
struct_vcpu = cast("struct kvm_vcpu *", entry)
if hex(struct_vcpu.value_()) == "0x0":
break
vcpu = hex(struct_vcpu.value_())
id = struct_vcpu.vcpu_id.value_()
idx = struct_vcpu.vcpu_idx.value_()
arch = hex(struct_vcpu.arch.address_of_())
stat = hex(struct_vcpu.stat.address_of_())
stat_id = struct_vcpu.stats_id.string_().decode("utf-8")
state = struct_vcpu.arch.mp_state.value_()
cpu = struct_vcpu.cpu.value_()
task = hex(struct_vcpu.wait.task.value_())

rows.append(
[
hex(vm.value_()),
vcpu,
id,
idx,
arch,
stat,
stat_id,
KVM_VCPU_STATE[state],
cpu,
str(task),
]
)
Comment on lines +135 to +204
Copy link
Member

Choose a reason for hiding this comment

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

Rather than having two distinct loops with a lot of duplicate code, we could clean this up. Let's define a for_each_vcpu() function like below:

def for_each_vcpu(vm: Object) -> Iterator[Object]:
    if has_member(vm, "vcpus"):
        vcpu_iterator = iter(vm.vcpus)
    else:
        vcpu_iterator = (
            cast("struct kvm_vcpu *", e)
            for _, e in xa_for_each(vm.vcpu_array.address_of_())
        )
    for struct_vcpu in vcpu_iterator:
        if struct_vcpu.value_() == 0:
            break
        yield struct_vcpu

Then you can use that helper here to consolidate, so that you don't repeat the printing code. Plus we get a nice helper for others to use.


print_table(rows)


def get_platform_arch(prog: Program) -> str:
Expand Down Expand Up @@ -93,6 +286,32 @@ class VirtUtil(CorelensModule):

name = "virt"

def add_args(self, parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"-kvm",
Copy link
Member

Choose a reason for hiding this comment

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

To make everything consistent in corelens, please use two hyphens to introduce a long option (e.g. --kvm, --vcpu. But -p can stay the same.)

dest="list_vm",
action="store_true",
help="show all VM info",
)
parser.add_argument(
"-vcpu",
dest="vcpu_list",
action="store_true",
help="show all vcpu info",
)
parser.add_argument(
"-p",
dest="show_platform",
action="store_true",
help="show platfrom related information",
)

def run(self, prog: Program, args: argparse.Namespace) -> None:
show_platform(prog)
if args.show_platform:
show_platform(prog)
elif args.list_vm:
print_vm_list(prog)
elif args.vcpu_list:
print_vcpu_list(prog)
Comment on lines +310 to +315
Copy link
Member

Choose a reason for hiding this comment

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

Is there any reason not to just print all of these by default?

I wouldn't expect more than a few hundred vCPU or VM even on a large machine, so I don't think there's a huge performance cost to enabling everything by default. Plus, that means default corelens reports will have the information automatically. (If we make them opt-in via CLI flags, then we need to specify defaults for corelens to use: see the NVME module).

Suggested change
if args.show_platform:
show_platform(prog)
elif args.list_vm:
print_vm_list(prog)
elif args.vcpu_list:
print_vcpu_list(prog)
show_platform(prog)
print_vm_list(prog)
print_vcpu_list(prog)


return
2 changes: 2 additions & 0 deletions tests/test_virtutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@
def test_virtutil(prog):
virtutil.show_cpuhp_state(prog)
virtutil.show_platform(prog)
virtutil.print_vm_list(prog)
virtutil.print_vcpu_list(prog)
Loading