From 4446f92307eb20f5ad9e980b91e2a994b2524be0 Mon Sep 17 00:00:00 2001 From: Benjamin Naecker Date: Wed, 14 Feb 2024 21:32:20 +0000 Subject: [PATCH] Remove actual VM and kstat implementations, they'll live in Propolis --- oximeter/instruments/Cargo.toml | 3 +- oximeter/instruments/src/kstat/mod.rs | 2 - .../instruments/src/kstat/virtual_machine.rs | 459 ------------------ .../tests/output/virtual-machine-schema.json | 49 -- 4 files changed, 1 insertion(+), 512 deletions(-) delete mode 100644 oximeter/instruments/src/kstat/virtual_machine.rs delete mode 100644 oximeter/instruments/tests/output/virtual-machine-schema.json diff --git a/oximeter/instruments/Cargo.toml b/oximeter/instruments/Cargo.toml index 5903f1da511..12827699f1f 100644 --- a/oximeter/instruments/Cargo.toml +++ b/oximeter/instruments/Cargo.toml @@ -18,7 +18,7 @@ uuid = { workspace = true, optional = true } omicron-workspace-hack = { workspace = true, optional = true } [features] -default = ["http-instruments", "datalink", "virtual-machine"] +default = ["http-instruments", "datalink"] http-instruments = [ "dep:chrono", "dep:dropshot", @@ -41,7 +41,6 @@ kstat = [ "dep:uuid" ] datalink = ["kstat"] -virtual-machine = ["kstat"] [dev-dependencies] rand.workspace = true diff --git a/oximeter/instruments/src/kstat/mod.rs b/oximeter/instruments/src/kstat/mod.rs index 0b102ebd6f0..46f0174da17 100644 --- a/oximeter/instruments/src/kstat/mod.rs +++ b/oximeter/instruments/src/kstat/mod.rs @@ -90,8 +90,6 @@ use std::time::Duration; #[cfg(any(feature = "datalink", test))] pub mod link; mod sampler; -#[cfg(any(feature = "virtual-machine", test))] -pub mod virtual_machine; pub use sampler::CollectionDetails; pub use sampler::ExpirationBehavior; diff --git a/oximeter/instruments/src/kstat/virtual_machine.rs b/oximeter/instruments/src/kstat/virtual_machine.rs deleted file mode 100644 index 1adec99aaa8..00000000000 --- a/oximeter/instruments/src/kstat/virtual_machine.rs +++ /dev/null @@ -1,459 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -// Copyright 2024 Oxide Computer Company - -//! Types for tracking statistics about virtual machine instances. - -use crate::kstat::hrtime_to_utc; -use crate::kstat::ConvertNamedData; -use crate::kstat::Error; -use chrono::DateTime; -use chrono::Utc; -use oximeter::types::Cumulative; -use oximeter::FieldType; -use oximeter::FieldValue; -use oximeter::Metric; -use oximeter::Sample; -use oximeter::Target; -use uuid::Uuid; - -#[cfg(not(test))] -mod kstat_types { - pub use crate::kstat::KstatList; - pub use crate::kstat::KstatTarget; - pub use kstat_rs::Data; - pub use kstat_rs::Kstat; - pub use kstat_rs::Named; - pub use kstat_rs::NamedData; -} - -// Mock the relevant subset of `kstat-rs` types needed for tests. -#[cfg(test)] -mod kstat_types { - #[derive(Debug)] - pub enum Data<'a> { - Named(Vec>), - #[allow(dead_code)] - Null, - } - - #[derive(Debug)] - pub enum NamedData<'a> { - UInt32(u32), - UInt64(u64), - String(&'a str), - } - - #[derive(Debug)] - pub struct Kstat<'a> { - pub ks_module: &'a str, - pub ks_instance: i32, - pub ks_name: &'a str, - pub ks_snaptime: i64, - } - - #[derive(Debug)] - pub struct Named<'a> { - pub name: &'a str, - pub value: NamedData<'a>, - } - - impl<'a> super::ConvertNamedData for NamedData<'a> { - fn as_i32(&self) -> Result { - unimplemented!() - } - - fn as_u32(&self) -> Result { - if let NamedData::UInt32(x) = self { - Ok(*x) - } else { - panic!() - } - } - - fn as_i64(&self) -> Result { - unimplemented!() - } - - fn as_u64(&self) -> Result { - if let NamedData::UInt64(x) = self { - Ok(*x) - } else { - panic!() - } - } - } -} - -use kstat_types::*; - -/// A single virtual machine instance. -#[derive(Clone, Debug)] -pub struct VirtualMachine { - /// The silo to which the instance belongs. - pub silo_id: Uuid, - /// The name of the silo to which the instance belongs. - pub silo_name: String, - /// The project to which the instance belongs. - pub project_id: Uuid, - /// The name of the project to which the instance belongs. - pub project_name: String, - /// The ID of the instance. - pub instance_id: Uuid, - /// The name of the instance. - pub instance_name: String, - - // This field is not published as part of the target field definitions. It - // is needed because the hypervisor currently creates kstats for each vCPU, - // regardless of whether they're activated. There is no way to tell from - // userland today which vCPU kstats are "real". We include this value here, - // and implement `oximeter::Target` manually, so that this field is not - // published as a field on the timeseries. - pub n_vcpus: u32, -} - -impl Target for VirtualMachine { - fn name(&self) -> &'static str { - "virtual_machine" - } - - fn field_names(&self) -> &'static [&'static str] { - &[ - "silo_id", - "silo_name", - "project_id", - "project_name", - "instance_id", - "instance_name", - ] - } - - fn field_types(&self) -> Vec { - vec![ - FieldType::Uuid, - FieldType::String, - FieldType::Uuid, - FieldType::String, - FieldType::Uuid, - FieldType::String, - ] - } - - fn field_values(&self) -> Vec { - vec![ - self.silo_id.into(), - self.silo_name.clone().into(), - self.project_id.into(), - self.project_name.clone().into(), - self.instance_id.into(), - self.instance_name.clone().into(), - ] - } -} - -/// Metric tracking vCPU usage by state. -#[derive(Clone, Debug, Metric)] -pub struct VcpuUsage { - /// The vCPU ID. - pub vcpu_id: u32, - /// The state of the vCPU. - pub state: String, - /// The cumulative time spent in this state, in nanoseconds. - pub datum: Cumulative, -} - -// The name of the kstat module containing virtual machine kstats. -const VMM_KSTAT_MODULE_NAME: &str = "vmm"; - -// The name of the kstat with virtual machine metadata (VM name currently). -const VM_KSTAT_NAME: &str = "vm"; - -// The named kstat holding the virtual machine's name. This is currently the -// UUID assigned by the control plane to the virtual machine instance. -const VM_NAME_KSTAT: &str = "vm_name"; - -// The name of kstat containing vCPU usage data. -const VCPU_KSTAT_PREFIX: &str = "vcpu"; - -// Prefix for all named data with a valid vCPU microstate that we track. -const VCPU_MICROSTATE_PREFIX: &str = "time_"; - -// The number of expected vCPU microstates we track. This isn't load-bearing, -// and only used to help preallocate an array holding the `VcpuUsage` samples. -const N_VCPU_MICROSTATES: usize = 6; - -#[cfg(not(test))] -impl KstatTarget for VirtualMachine { - // The VMM kstats are organized like so: - // - // - module: vmm - // - instance: a kernel-assigned integer - // - name: vm -> generic VM info, vcpuX -> info for each vCPU - // - // At this part of the code, we don't have that kstat instance, only the - // virtual machine instance's control plane UUID. However, the VM's "name" - // is assigned to be that control plane UUID in the hypervisor. See - // https://github.com/oxidecomputer/propolis/blob/759bf4a19990404c135e608afbe0d38b70bfa370/bin/propolis-server/src/lib/vm/mod.rs#L420 - // for the current code which does that. - // - // That means we need to indicate interest in both the `vm` and `vcpuX` - // kstats for any instance, and then filter to the right instance in the - // `to_samples()` method below, because interest is defined on each - // individual kstat. - fn interested(&self, kstat: &Kstat<'_>) -> bool { - kstat.ks_module == VMM_KSTAT_MODULE_NAME - } - - fn to_samples( - &self, - kstats: KstatList<'_, '_>, - ) -> Result, Error> { - // First, we need to map the instance's control plane UUID to the kstat - // instance. We'll find this through the `vmm::vm:vm_name` - // kstat, which lists the instance's UUID as its name. - // - // Note that if this code is run from within a Propolis zone, there is - // exactly one `vmm` kstat instance in any case. - let instance_id = self.instance_id.to_string(); - let instance = kstats - .iter() - .find_map(|(_, kstat, data)| { - kstat_instance_from_instance_id(kstat, data, &instance_id) - }) - .ok_or_else(|| Error::NoSuchKstat)?; - - // Armed with the kstat instance, find all relevant metrics related to - // this particular VM. For now, we produce only vCPU usage metrics, but - // others may be chained in the future. - let vcpu_stats = kstats.iter().filter(|(_, kstat, _)| { - // Filter out those that don't match our kstat instance. - if kstat.ks_instance != instance { - return false; - } - - // Filter out those which are neither a vCPU stat of any kind, nor - // for one of the vCPU IDs we know to be active. - let Some(suffix) = kstat.ks_name.strip_prefix(VCPU_KSTAT_PREFIX) - else { - return false; - }; - let Ok(vcpu_id) = suffix.parse::() else { - return false; - }; - vcpu_id < self.n_vcpus - }); - produce_vcpu_usage(self, vcpu_stats) - } -} - -// Given a kstat and an instance's ID, return the kstat instance if it matches. -fn kstat_instance_from_instance_id( - kstat: &Kstat<'_>, - data: &Data<'_>, - instance_id: &str, -) -> Option { - // Filter out anything that's not a `vmm::vm` named kstat. - if kstat.ks_module != VMM_KSTAT_MODULE_NAME { - return None; - } - if kstat.ks_name != VM_KSTAT_NAME { - return None; - } - let Data::Named(named) = data else { - return None; - }; - - // Return the instance if the `vm_name` kstat matches our instance UUID. - if named.iter().any(|nd| { - if nd.name != VM_NAME_KSTAT { - return false; - } - let NamedData::String(name) = &nd.value else { - return false; - }; - instance_id == *name - }) { - return Some(kstat.ks_instance); - } - None -} - -// Produce `Sample`s for the `VcpuUsage` metric from the relevant kstats. -fn produce_vcpu_usage<'a>( - vm: &'a VirtualMachine, - vcpu_stats: impl Iterator, Kstat<'a>, Data<'a>)> + 'a, -) -> Result, Error> { - let mut out = Vec::with_capacity(vm.n_vcpus as usize * N_VCPU_MICROSTATES); - for (creation_time, kstat, data) in vcpu_stats { - let Data::Named(named) = data else { - return Err(Error::ExpectedNamedKstat); - }; - let snapshot_time = hrtime_to_utc(kstat.ks_snaptime)?; - - // Find the vCPU ID, from the `vmm::vcpuX:vcpu` named kstat. - let vcpu_id = named - .iter() - .find_map(|named| { - if named.name == VCPU_KSTAT_PREFIX { - named.value.as_u32().ok() - } else { - None - } - }) - .ok_or_else(|| Error::NoSuchKstat)?; - - // We'll track all statistics starting with `time_` as the microstate, - // and extract the cumulative occupancy time as a u64. - for (state, value) in named.iter().filter_map(|nv| { - nv.name - .strip_prefix(VCPU_MICROSTATE_PREFIX) - .map(|state| (state.to_string(), &nv.value)) - }) { - let datum = - Cumulative::with_start_time(*creation_time, value.as_u64()?); - let metric = VcpuUsage { vcpu_id, state, datum }; - let sample = - Sample::new_with_timestamp(snapshot_time, vm, &metric)?; - out.push(sample); - } - } - Ok(out) -} - -#[cfg(test)] -mod tests { - use super::kstat_instance_from_instance_id; - use super::produce_vcpu_usage; - use super::Data; - use super::Kstat; - use super::Named; - use super::NamedData; - use super::Utc; - use super::VcpuUsage; - use super::VirtualMachine; - use super::VCPU_KSTAT_PREFIX; - use super::VMM_KSTAT_MODULE_NAME; - use super::VM_KSTAT_NAME; - use super::VM_NAME_KSTAT; - use oximeter::schema::SchemaSet; - use oximeter::types::Cumulative; - use oximeter::Datum; - use oximeter::FieldValue; - - fn test_virtual_machine() -> VirtualMachine { - VirtualMachine { - silo_id: uuid::uuid!("6a4bd4b6-e9aa-44d1-b616-399d48baa173"), - silo_name: String::from("silo-name"), - project_id: uuid::uuid!("7b61df02-0794-4b37-93bc-89f03c7289ca"), - project_name: String::from("project-name"), - instance_id: uuid::uuid!("96d6ec78-543a-4188-830e-37e2a0eeff16"), - instance_name: String::from("instance-name"), - n_vcpus: 4, - } - } - - fn test_usage() -> VcpuUsage { - VcpuUsage { - state: "run".to_string(), - vcpu_id: 0, - datum: Cumulative::new(100), - } - } - - #[test] - fn test_no_schema_changes() { - let mut set = SchemaSet::default(); - assert!(set - .insert_checked(&test_virtual_machine(), &test_usage()) - .is_none()); - const PATH: &str = concat!( - env!("CARGO_MANIFEST_DIR"), - "/tests/output/virtual-machine-schema.json", - ); - set.assert_contents(PATH); - } - - #[test] - fn test_kstat_instance_from_instance_id() { - let ks = Kstat { - ks_module: VMM_KSTAT_MODULE_NAME, - ks_instance: 0, - ks_name: VM_KSTAT_NAME, - ks_snaptime: 1, - }; - const INSTANCE_ID: &str = "db198b43-2dee-4b4b-8a68-24cb4c0d6ec8"; - let data = Data::Named(vec![Named { - name: VM_NAME_KSTAT, - value: NamedData::String(INSTANCE_ID), - }]); - - assert_eq!( - kstat_instance_from_instance_id(&ks, &data, INSTANCE_ID) - .expect("Should have matched the instance ID"), - ks.ks_instance, - ); - - let data = Data::Named(vec![Named { - name: VM_NAME_KSTAT, - value: NamedData::String("something-else"), - }]); - assert!( - kstat_instance_from_instance_id(&ks, &data, INSTANCE_ID).is_none(), - "Should not have matched an instance ID" - ); - } - - fn vcpu_state_kstats<'a>() -> (Kstat<'a>, Data<'a>) { - let ks = Kstat { - ks_module: VMM_KSTAT_MODULE_NAME, - ks_instance: 0, - ks_name: "vcpu0", - ks_snaptime: 1, - }; - let data = Data::Named(vec![ - Named { name: VCPU_KSTAT_PREFIX, value: NamedData::UInt32(0) }, - Named { name: "time_idle", value: NamedData::UInt64(1) }, - Named { name: "time_run", value: NamedData::UInt64(2) }, - ]); - (ks, data) - } - - #[test] - fn test_produce_vcpu_usage() { - let (ks, data) = vcpu_state_kstats(); - let kstats = [(Utc::now(), ks, data)]; - let samples = - produce_vcpu_usage(&test_virtual_machine(), kstats.iter()) - .expect("Should have produced samples"); - assert_eq!( - samples.len(), - 2, - "Should have samples for 'run' and 'idle' states" - ); - for ((sample, state), x) in - samples.iter().zip(["idle", "run"]).zip([1, 2]) - { - let st = sample - .fields() - .iter() - .find_map(|f| { - if f.name == "state" { - let FieldValue::String(state) = &f.value else { - panic!("Expected a string field"); - }; - Some(state.clone()) - } else { - None - } - }) - .expect("expected a field with name \"state\""); - assert_eq!(st, state, "Found an incorrect vCPU state"); - let Datum::CumulativeU64(inner) = sample.measurement.datum() else { - panic!("Expected a cumulativeu64 datum"); - }; - assert_eq!(inner.value(), x); - } - } -} diff --git a/oximeter/instruments/tests/output/virtual-machine-schema.json b/oximeter/instruments/tests/output/virtual-machine-schema.json deleted file mode 100644 index baa7aa76b1e..00000000000 --- a/oximeter/instruments/tests/output/virtual-machine-schema.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "virtual_machine:vcpu_usage": { - "timeseries_name": "virtual_machine:vcpu_usage", - "field_schema": [ - { - "name": "instance_id", - "field_type": "uuid", - "source": "target" - }, - { - "name": "instance_name", - "field_type": "string", - "source": "target" - }, - { - "name": "project_id", - "field_type": "uuid", - "source": "target" - }, - { - "name": "project_name", - "field_type": "string", - "source": "target" - }, - { - "name": "silo_id", - "field_type": "uuid", - "source": "target" - }, - { - "name": "silo_name", - "field_type": "string", - "source": "target" - }, - { - "name": "state", - "field_type": "string", - "source": "metric" - }, - { - "name": "vcpu_id", - "field_type": "u32", - "source": "metric" - } - ], - "datum_type": "cumulative_u64", - "created": "2024-01-29T19:51:22.103250716Z" - } -} \ No newline at end of file