Skip to content
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
64 changes: 53 additions & 11 deletions bin/mock-server/src/lib/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::InstanceStateChange>,
}

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 Down Expand Up @@ -126,7 +128,8 @@ impl InstanceContext {
}
(
api::InstanceState::Running,
api::InstanceStateRequested::Reboot,
api::InstanceStateRequested::Reboot
| api::InstanceStateRequested::AcpiReboot,
) => {
self.queue_states(
log,
Expand All @@ -138,12 +141,26 @@ impl InstanceContext {
.await;
Ok(())
}
(current, api::InstanceStateRequested::Reboot) => {
Err(Error::InvalidTransition {
current,
requested: api::InstanceStateRequested::Reboot,
})
(
api::InstanceState::Running,
api::InstanceStateRequested::AcpiStop,
) => {
self.queue_states(
log,
&[
api::InstanceState::Stopping,
api::InstanceState::Stopped,
],
)
.await;
Ok(())
}
(
current,
api::InstanceStateRequested::Reboot
| api::InstanceStateRequested::AcpiReboot
| api::InstanceStateRequested::AcpiStop,
) => Err(Error::InvalidTransition { current, requested: target }),
(_, api::InstanceStateRequested::Run) => {
self.queue_states(log, &[api::InstanceState::Running]).await;
Ok(())
Expand Down Expand Up @@ -344,7 +361,7 @@ async fn instance_state_monitor(
}]
async fn instance_state_put(
rqctx: RequestContext<Arc<Context>>,
request: TypedBody<api::InstanceStateRequested>,
request: TypedBody<api::InstanceStateChange>,
) -> Result<HttpResponseUpdatedNoContent, HttpError> {
let mut instance = rqctx.context().instance.lock().await;
let instance = instance.as_mut().ok_or_else(|| {
Expand All @@ -353,13 +370,15 @@ async fn instance_state_put(
)
})?;
let requested_state = request.into_inner();
instance.set_target_state(&rqctx.log, requested_state).await.map_err(
|err| {
instance
.set_target_state(&rqctx.log, requested_state.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 +575,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::InstanceStateChange>, 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 +873,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
20 changes: 16 additions & 4 deletions bin/propolis-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ use uuid::Uuid;

use propolis_client::{
support::{InstanceSerialConsoleHelper, WSClientOffset},
types::{InstanceStateRequested, InstanceVcrReplace, MigrationState},
types::{
InstanceStateChange, InstanceStateRequested, InstanceVcrReplace,
MigrationState,
},
Client,
};

Expand Down Expand Up @@ -118,6 +121,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 +452,11 @@ fn parse_state(state: &str) -> anyhow::Result<InstanceStateRequested> {
"run" => Ok(InstanceStateRequested::Run),
"stop" => Ok(InstanceStateRequested::Stop),
"reboot" => Ok(InstanceStateRequested::Reboot),
"acpi-stop" => Ok(InstanceStateRequested::AcpiStop),
"acpi-reboot" => Ok(InstanceStateRequested::AcpiReboot),
_ => 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 @@ -572,10 +581,11 @@ async fn get_instance(client: &Client) -> anyhow::Result<()> {
async fn put_instance(
client: &Client,
state: InstanceStateRequested,
acpi_timeout_secs: Option<u64>,
) -> anyhow::Result<()> {
client
.instance_state_put()
.body(state)
.body(InstanceStateChange { state, acpi_timeout_secs })
.send()
.await
.with_context(|| anyhow!("failed to set instance state"))?;
Expand Down Expand Up @@ -958,7 +968,9 @@ async fn main() -> anyhow::Result<()> {
.await?
}
Command::Get => get_instance(&client).await?,
Command::State { state } => put_instance(&client, state).await?,
Command::State { state, acpi_timeout_secs } => {
put_instance(&client, state, acpi_timeout_secs).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
10 changes: 5 additions & 5 deletions bin/propolis-server/src/lib/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ use propolis_api_types::disk::{
};
use propolis_api_types::instance::{
ErrorCode, Instance, InstanceEnsureRequest, InstanceEnsureResponse,
InstanceGetResponse, InstanceInitializationMethod,
InstanceGetResponse, InstanceInitializationMethod, InstanceStateChange,
InstanceStateMonitorRequest, InstanceStateMonitorResponse,
InstanceStateRequested,
};
Expand Down Expand Up @@ -364,13 +364,13 @@ impl PropolisServerApi for PropolisServerImpl {

async fn instance_state_put(
rqctx: RequestContext<Self::Context>,
request: TypedBody<InstanceStateRequested>,
request: TypedBody<InstanceStateChange>,
) -> 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.state {
let stats = MutexGuard::map(
vm.services().oximeter.lock().await,
|state| &mut state.stats,
Expand Down
41 changes: 31 additions & 10 deletions bin/propolis-server/src/lib/vm/active.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@
//! Implements a wrapper around an active VM.

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

use propolis_api_types::instance::{
InstanceProperties, InstanceStateRequested,
InstanceProperties, InstanceStateChange, InstanceStateRequested,
};
use propolis_api_types::instance_spec::SpecKey;
use slog::info;
use uuid::Uuid;

use crate::vm::request_queue::ExternalRequest;
use crate::vm::request_queue::{ExternalRequest, RequestDeniedReason};
use crate::vm::SoftShutdownFate;

use super::{
objects::VmObjects, services::VmServices, CrucibleReplaceResultTx,
Expand Down Expand Up @@ -57,17 +59,36 @@ impl ActiveVm {
/// Pushes a state change request to the VM's state change queue.
pub(crate) fn put_state(
&self,
requested: InstanceStateRequested,
requested: InstanceStateChange,
) -> Result<(), VmError> {
info!(self.log, "requested state via API";
"state" => ?requested);

"state" => ?requested.state,
"timeout" => ?requested.acpi_timeout_secs);

let timeout = requested.acpi_timeout_secs.map(Duration::from_secs);
let ext_req = match requested.state {
InstanceStateRequested::Run
| InstanceStateRequested::Stop
| InstanceStateRequested::Reboot
if timeout.is_some() =>
{
return Err(RequestDeniedReason::TimeoutOnNonAcpiState.into());
}
InstanceStateRequested::Run => ExternalRequest::start(),
InstanceStateRequested::Stop => ExternalRequest::stop(),
InstanceStateRequested::Reboot => ExternalRequest::reboot(),
InstanceStateRequested::AcpiStop => {
ExternalRequest::acpi_shutdown(SoftShutdownFate::Stop, timeout)
}
InstanceStateRequested::AcpiReboot => {
ExternalRequest::acpi_shutdown(
SoftShutdownFate::Reboot,
timeout,
)
}
};
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
Loading
Loading