Skip to content
Open
77 changes: 68 additions & 9 deletions bin/mock-server/src/lib/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ mod api_types;
use api_types::types::{self as api, InstanceEnsureRequest};
pub use api_types::MockMode;

#[derive(Debug, Eq, PartialEq, Error)]
#[derive(Debug, Error)]
pub enum Error {
#[error("Failed to send simulated state change update through channel")]
TransitionSendFail,
Expand All @@ -47,6 +47,7 @@ pub struct InstanceContext {
serial_task: serial::SerialTask,
state_watcher_rx: watch::Receiver<MockState>,
state_watcher_tx: watch::Sender<MockState>,
last_state_change: Option<api::InstanceStateRequested>,
}

struct MockState {
Expand Down Expand Up @@ -92,6 +93,7 @@ impl InstanceContext {
serial_task,
state_watcher_rx,
state_watcher_tx,
last_state_change: None,
}
}

Expand All @@ -101,7 +103,7 @@ impl InstanceContext {
pub async fn set_target_state(
&mut self,
log: &Logger,
target: api::InstanceStateRequested,
target: &api::InstanceStateRequested,
) -> Result<(), Error> {
match (self.current_state(), target) {
(
Expand All @@ -126,7 +128,8 @@ impl InstanceContext {
}
(
api::InstanceState::Running,
api::InstanceStateRequested::Reboot,
api::InstanceStateRequested::Reboot
| api::InstanceStateRequested::AcpiReboot { timeout_secs: _ },
) => {
self.queue_states(
log,
Expand All @@ -138,12 +141,44 @@ impl InstanceContext {
.await;
Ok(())
}
(current, api::InstanceStateRequested::Reboot) => {
Err(Error::InvalidTransition {
current,
requested: api::InstanceStateRequested::Reboot,
})
(
api::InstanceState::Running,
api::InstanceStateRequested::AcpiStop { timeout_secs: _ },
) => {
self.queue_states(
log,
&[
api::InstanceState::Stopping,
api::InstanceState::Stopped,
],
)
.await;
Ok(())
}
// allowed to change the fate of an ACPI shutdown to a reboot
(
api::InstanceState::Stopping,
api::InstanceStateRequested::Reboot
| api::InstanceStateRequested::AcpiReboot { timeout_secs: _ },
) if matches!(
self.last_state_change.as_ref(),
Some(
api::InstanceStateRequested::AcpiStop { .. }
| api::InstanceStateRequested::AcpiReboot { .. }
)
) =>
{
Ok(())
}
(
current,
api::InstanceStateRequested::Reboot
| api::InstanceStateRequested::AcpiReboot { timeout_secs: _ }
| api::InstanceStateRequested::AcpiStop { timeout_secs: _ },
) => Err(Error::InvalidTransition {
current,
requested: target.to_owned(),
}),
(_, api::InstanceStateRequested::Run) => {
self.queue_states(log, &[api::InstanceState::Running]).await;
Ok(())
Expand Down Expand Up @@ -353,13 +388,14 @@ async fn instance_state_put(
)
})?;
let requested_state = request.into_inner();
instance.set_target_state(&rqctx.log, requested_state).await.map_err(
instance.set_target_state(&rqctx.log, &requested_state).await.map_err(
|err| {
HttpError::for_internal_error(format!(
"Failed to transition: {err}"
))
},
)?;
instance.last_state_change = Some(requested_state);
Ok(HttpResponseUpdatedNoContent {})
}

Expand Down Expand Up @@ -556,6 +592,28 @@ async fn mock_step(
Ok(HttpResponseUpdatedNoContent())
}

// used for checking acpi shutdown timeout values in controlplane tests
#[endpoint {
method = GET,
path = "/mock/last_state_requested"
}]
async fn mock_last_state_requested_get(
rqctx: RequestContext<Arc<Context>>,
) -> Result<HttpResponseOk<api::InstanceStateRequested>, HttpError> {
let instance = rqctx.context().instance.lock().await;
let instance = instance.as_ref().ok_or_else(|| {
HttpError::for_internal_error(
"Server not initialized (no instance)".to_string(),
)
})?;
match &instance.last_state_change {
Some(change) => Ok(HttpResponseOk(change.to_owned())),
None => Err(HttpError::for_internal_error(
"No instance_state_put yet received".to_string(),
)),
}
}

mod serial {
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
Expand Down Expand Up @@ -832,6 +890,7 @@ pub fn api() -> ApiDescription<Arc<Context>> {
api.register(mock_mode_get).unwrap();
api.register(mock_mode_set).unwrap();
api.register(mock_step).unwrap();
api.register(mock_last_state_requested_get).unwrap();
api
}

Expand Down
24 changes: 22 additions & 2 deletions bin/propolis-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ enum Command {
/// The requested state
#[clap(value_parser = parse_state)]
state: InstanceStateRequested,
/// The number of seconds to wait after sending ACPI PWRBTN_STS before
/// forcing stop/reset. (Only valid on acpi-stop and acpi-reboot states)
acpi_timeout_secs: Option<u64>,
},

/// Drop to a Serial console connected to the instance
Expand Down Expand Up @@ -446,8 +449,14 @@ fn parse_state(state: &str) -> anyhow::Result<InstanceStateRequested> {
"run" => Ok(InstanceStateRequested::Run),
"stop" => Ok(InstanceStateRequested::Stop),
"reboot" => Ok(InstanceStateRequested::Reboot),
// XXX: timeouts to be replaced after parse
"acpi-stop" => Ok(InstanceStateRequested::AcpiStop { timeout_secs: 0 }),
"acpi-reboot" => {
Ok(InstanceStateRequested::AcpiReboot { timeout_secs: 0 })
}
_ => Err(anyhow!(
"invalid requested state, must be one of: 'run', 'stop', 'reboot"
"invalid requested state, must be one of: \
'run', 'stop', 'reboot', 'acpi-stop', 'acpi-reboot'"
)),
}
}
Expand Down Expand Up @@ -958,7 +967,18 @@ async fn main() -> anyhow::Result<()> {
.await?
}
Command::Get => get_instance(&client).await?,
Command::State { state } => put_instance(&client, state).await?,
Command::State { mut state, acpi_timeout_secs } => {
match &mut state {
InstanceStateRequested::AcpiStop { timeout_secs }
| InstanceStateRequested::AcpiReboot { timeout_secs } => {
*timeout_secs = acpi_timeout_secs.ok_or(anyhow!(
"acpi-stop and acpi-reboot states must provide timeout"
))?
}
_ => {}
}
put_instance(&client, state).await?
}
Command::Serial { byte_offset } => {
serial(addr, byte_offset, log).await?
}
Expand Down
13 changes: 11 additions & 2 deletions bin/propolis-server/src/lib/initializer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ pub fn build_instance(
pub struct RegisteredChipset {
chipset: Arc<dyn Chipset>,
isa: Arc<i440fx::Piix3Lpc>,
pm: Arc<i440fx::Piix3PM>,
}
impl RegisteredChipset {
pub fn pci_attach(&self, bdf: pci::Bdf, dev: Arc<dyn pci::Endpoint>) {
Expand All @@ -197,6 +198,9 @@ impl RegisteredChipset {
pub fn irq_pin(&self, irq: u8) -> Option<Box<dyn intr_pins::IntrPin>> {
self.isa.irq_pin(irq)
}
pub fn acpi_shutdown(&self) {
self.pm.acpi_shutdown();
}
fn reset_pin(&self) -> Arc<dyn intr_pins::IntrPin> {
self.chipset.reset_pin()
}
Expand Down Expand Up @@ -356,6 +360,7 @@ impl MachineInitializer<'_> {
let chipset_pm = i440fx::Piix3PM::create(
self.machine.hdl.clone(),
chipset_hb.power_pin(),
chipset_lpc.sci_pin(),
self.log.new(slog::o!("device" => "piix3pm")),
);

Expand Down Expand Up @@ -387,7 +392,7 @@ impl MachineInitializer<'_> {
);
self.devices.insert(
SpecKey::Name(chipset_pm.type_name().into()),
chipset_pm,
chipset_pm.clone(),
);

// Record attachment for any bridges in PCI topology too
Expand All @@ -404,7 +409,11 @@ impl MachineInitializer<'_> {
self.devices.insert(spec_element.0.clone(), bridge);
}

Ok(RegisteredChipset { chipset: chipset_hb, isa: chipset_lpc })
Ok(RegisteredChipset {
chipset: chipset_hb,
isa: chipset_lpc,
pm: chipset_pm,
})
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions bin/propolis-server/src/lib/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -367,10 +367,10 @@ impl PropolisServerApi for PropolisServerImpl {
request: TypedBody<InstanceStateRequested>,
) -> Result<HttpResponseUpdatedNoContent, HttpError> {
let ctx = rqctx.context();
let requested_state = request.into_inner();
let requested_change = request.into_inner();
let vm = ctx.vm.active_vm().await.ok_or_else(not_created_error)?;
let result = vm
.put_state(requested_state)
.put_state(requested_change)
.map(|_| HttpResponseUpdatedNoContent {})
.map_err(|e| match e {
VmError::WaitingToInitialize => HttpError::for_unavail(
Expand All @@ -391,7 +391,7 @@ impl PropolisServerApi for PropolisServerImpl {
});

if result.is_ok() {
if let InstanceStateRequested::Reboot = requested_state {
if let InstanceStateRequested::Reboot = requested_change {
let stats = MutexGuard::map(
vm.services().oximeter.lock().await,
|state| &mut state.stats,
Expand Down
25 changes: 20 additions & 5 deletions bin/propolis-server/src/lib/vm/active.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
//! Implements a wrapper around an active VM.

use std::sync::Arc;
use std::time::Duration;

use propolis_api_types::instance::{
InstanceProperties, InstanceStateRequested,
Expand All @@ -14,6 +15,7 @@ use slog::info;
use uuid::Uuid;

use crate::vm::request_queue::ExternalRequest;
use crate::vm::SoftShutdownFate;

use super::{
objects::VmObjects, services::VmServices, CrucibleReplaceResultTx,
Expand Down Expand Up @@ -62,12 +64,25 @@ impl ActiveVm {
info!(self.log, "requested state via API";
"state" => ?requested);

let ext_req = match requested {
InstanceStateRequested::Run => ExternalRequest::start(),
InstanceStateRequested::Stop => ExternalRequest::stop(),
InstanceStateRequested::Reboot => ExternalRequest::reboot(),
InstanceStateRequested::AcpiStop { timeout_secs } => {
ExternalRequest::acpi_shutdown(
SoftShutdownFate::Stop,
Some(Duration::from_secs(timeout_secs)),
)
}
InstanceStateRequested::AcpiReboot { timeout_secs } => {
ExternalRequest::acpi_shutdown(
SoftShutdownFate::Reboot,
Some(Duration::from_secs(timeout_secs)),
)
}
};
self.state_driver_queue
.queue_external_request(match requested {
InstanceStateRequested::Run => ExternalRequest::start(),
InstanceStateRequested::Stop => ExternalRequest::stop(),
InstanceStateRequested::Reboot => ExternalRequest::reboot(),
})
.queue_external_request(ext_req)
.map_err(Into::into)
}

Expand Down
1 change: 1 addition & 0 deletions bin/propolis-server/src/lib/vm/ensure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,7 @@ async fn initialize_vm_objects(
framebuffer: Some(ramfb),
ps2ctrl,
attest_handle,
chipset: chipset.into(),
};

// Another really terrible hack. As we've found in Propolis#1008, brk()
Expand Down
7 changes: 7 additions & 0 deletions bin/propolis-server/src/lib/vm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,13 @@ const VMM_MIN_RT_THREADS: usize = 8;
/// is greater.
const VMM_BASE_RT_THREADS: usize = 4;

/// The action to take after an ACPI shutdown either completes or times out.
#[derive(Clone, Copy, Debug)]
pub enum SoftShutdownFate {
Stop,
Reboot,
}

/// Errors generated by the VM controller and its subcomponents.
#[derive(Debug, thiserror::Error)]
pub(crate) enum VmError {
Expand Down
15 changes: 14 additions & 1 deletion bin/propolis-server/src/lib/vm/objects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ use propolis_api_types::instance_spec::SpecKey;
use slog::info;
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};

use crate::{serial::Serial, spec::Spec, vcpu_tasks::VcpuTaskController};
use crate::{
initializer::RegisteredChipset, serial::Serial, spec::Spec,
vcpu_tasks::VcpuTaskController,
};

use super::{BlockBackendMap, CrucibleBackendMap, DeviceMap};

Expand Down Expand Up @@ -53,6 +56,7 @@ pub(super) struct InputVmObjects {
pub framebuffer: Option<Arc<RamFb>>,
pub ps2ctrl: Arc<PS2Ctrl>,
pub attest_handle: Option<attestation::server::AttestationSock>,
pub chipset: Arc<RegisteredChipset>,
}

/// The collection of objects and state that make up a Propolis instance.
Expand Down Expand Up @@ -91,6 +95,9 @@ pub(crate) struct VmObjectsLocked {

/// A handle to the VM's attestation server.
attest_handle: Option<attestation::server::AttestationSock>,

/// A handle to the VM's mainboard chipset.
chipset: Arc<RegisteredChipset>,
}

impl VmObjects {
Expand Down Expand Up @@ -132,6 +139,7 @@ impl VmObjectsLocked {
framebuffer: input.framebuffer,
ps2ctrl: input.ps2ctrl,
attest_handle: input.attest_handle,
chipset: input.chipset,
}
}

Expand Down Expand Up @@ -194,6 +202,11 @@ impl VmObjectsLocked {
&self.ps2ctrl
}

/// Yields a clonable reference to this VM's mainboard chipset.
pub(crate) fn chipset(&self) -> &Arc<RegisteredChipset> {
&self.chipset
}

pub(crate) fn device_map(&self) -> &DeviceMap {
&self.devices
}
Expand Down
Loading
Loading