diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index ae7e7dde2..16942bff1 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -54,10 +54,10 @@ jobs: - uses: dtolnay/rust-toolchain@439cf607258077187679211f12aa6f19af4a0af7 # master @ 2023-10-08 with: toolchain: stable - - name: Build mock-only server - run: cargo build --bin propolis-server --features mock-only - name: Build run: cargo build --verbose + - name: Build mock-only server + run: cargo build -p propolis-mock-server --verbose - name: Test Libraries run: cargo test --lib --verbose diff --git a/Cargo.lock b/Cargo.lock index e3ad453f6..ed46f0527 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3113,6 +3113,29 @@ dependencies = [ "uuid", ] +[[package]] +name = "propolis-mock-server" +version = "0.0.0" +dependencies = [ + "anyhow", + "atty", + "base64 0.21.4", + "clap 4.4.0", + "dropshot", + "futures", + "hyper", + "propolis-client", + "serde_json", + "slog", + "slog-async", + "slog-bunyan", + "slog-dtrace", + "slog-term", + "thiserror", + "tokio", + "tokio-tungstenite", +] + [[package]] name = "propolis-package" version = "0.1.0" @@ -3134,7 +3157,6 @@ dependencies = [ "bit_field", "bitvec", "bytes", - "cfg-if", "chrono", "clap 4.4.0", "const_format", diff --git a/bin/mock-server/Cargo.toml b/bin/mock-server/Cargo.toml new file mode 100644 index 000000000..253cc0eb0 --- /dev/null +++ b/bin/mock-server/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "propolis-mock-server" +version = "0.0.0" +license = "MPL-2.0" +edition = "2021" + +[lib] +name = "propolis_mock_server" +path = "src/lib/lib.rs" +doc = false +doctest = false +test = false + +[[bin]] +name = "propolis-mock-server" +path = "src/main.rs" +doc = false +doctest = false +test = false + +[dependencies] +atty.workspace = true +anyhow.workspace = true +clap = { workspace = true, features = ["derive"] } +base64.workspace = true +dropshot = { workspace = true } +futures.workspace = true +hyper.workspace = true +propolis-client = { workspace = true, features = ["generated"] } +serde_json.workspace = true +slog.workspace = true +slog-async.workspace = true +slog-dtrace.workspace = true +slog-term.workspace = true +slog-bunyan.workspace = true +thiserror.workspace = true +tokio = { workspace = true, features = ["full"] } +tokio-tungstenite.workspace = true diff --git a/bin/mock-server/src/lib/copied.rs b/bin/mock-server/src/lib/copied.rs new file mode 100644 index 000000000..ad5bd6e94 --- /dev/null +++ b/bin/mock-server/src/lib/copied.rs @@ -0,0 +1,64 @@ +// 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/. + +//! Bits copied from propolis-server, rather than splitting them out into some +//! shared dependency + +use propolis_client::handmade::api; +use propolis_client::instance_spec::{v0::builder::SpecBuilderError, PciPath}; + +use thiserror::Error; + +#[derive(Clone, Copy, Debug)] +pub(crate) enum SlotType { + Nic, + Disk, + CloudInit, +} + +#[allow(unused)] +#[derive(Debug, Error)] +pub(crate) enum ServerSpecBuilderError { + #[error(transparent)] + InnerBuilderError(#[from] SpecBuilderError), + + #[error("The string {0} could not be converted to a PCI path")] + PciPathNotParseable(String), + + #[error( + "Could not translate PCI slot {0} for device type {1:?} to a PCI path" + )] + PciSlotInvalid(u8, SlotType), + + #[error("Unrecognized storage device interface {0}")] + UnrecognizedStorageDevice(String), + + #[error("Unrecognized storage backend type {0}")] + UnrecognizedStorageBackend(String), + + #[error("Device {0} requested missing backend {1}")] + DeviceMissingBackend(String, String), + + #[error("Error in server config TOML: {0}")] + ConfigTomlError(String), + + #[error("Error serializing {0} into spec element: {1}")] + SerializationError(String, serde_json::error::Error), +} + +pub(crate) fn slot_to_pci_path( + slot: api::Slot, + ty: SlotType, +) -> Result { + match ty { + // Slots for NICS: 0x08 -> 0x0F + SlotType::Nic if slot.0 <= 7 => PciPath::new(0, slot.0 + 0x8, 0), + // Slots for Disks: 0x10 -> 0x17 + SlotType::Disk if slot.0 <= 7 => PciPath::new(0, slot.0 + 0x10, 0), + // Slot for CloudInit + SlotType::CloudInit if slot.0 == 0 => PciPath::new(0, slot.0 + 0x18, 0), + _ => return Err(ServerSpecBuilderError::PciSlotInvalid(slot.0, ty)), + } + .map_err(|_| ServerSpecBuilderError::PciSlotInvalid(slot.0, ty)) +} diff --git a/bin/propolis-server/src/lib/mock_server.rs b/bin/mock-server/src/lib/lib.rs similarity index 51% rename from bin/propolis-server/src/lib/mock_server.rs rename to bin/mock-server/src/lib/lib.rs index 12e96eb3c..9a15984b2 100644 --- a/bin/propolis-server/src/lib/mock_server.rs +++ b/bin/mock-server/src/lib/lib.rs @@ -4,39 +4,27 @@ //! Implementation of a mock Propolis server +use std::io::{Error as IoError, ErrorKind}; +use std::sync::Arc; + use base64::Engine; -use dropshot::endpoint; -use dropshot::ApiDescription; -use dropshot::HttpError; -use dropshot::HttpResponseCreated; -use dropshot::HttpResponseOk; -use dropshot::HttpResponseUpdatedNoContent; -use dropshot::RequestContext; -use dropshot::TypedBody; -use dropshot::WebsocketConnection; -use dropshot::{channel, Query}; +use dropshot::{ + channel, endpoint, ApiDescription, HttpError, HttpResponseCreated, + HttpResponseOk, HttpResponseUpdatedNoContent, Query, RequestContext, + TypedBody, WebsocketConnection, +}; use futures::SinkExt; +use propolis_client::handmade::api; use slog::{error, info, Logger}; -use std::collections::VecDeque; -use std::convert::TryFrom; -use std::hash::{Hash, Hasher}; -use std::io::{Error as IoError, ErrorKind}; -use std::num::NonZeroUsize; -use std::sync::Arc; use thiserror::Error; -use tokio::sync::{mpsc, watch, Mutex}; -use tokio_tungstenite::tungstenite::protocol::WebSocketConfig; -use tokio_tungstenite::tungstenite::{protocol::Role, Message}; +use tokio::sync::{watch, Mutex}; +use tokio_tungstenite::tungstenite::protocol::{Role, WebSocketConfig}; +use tokio_tungstenite::tungstenite::Message; use tokio_tungstenite::WebSocketStream; -use propolis::chardev; -use propolis::chardev::{SinkNotifier, SourceNotifier}; -use propolis_client::handmade::api; +mod copied; -use crate::config::Config; -use crate::serial::history_buffer::SerialHistoryOffset; -use crate::serial::{Serial, SerialTask}; -use crate::spec::{slot_to_pci_path, SlotType}; +use crate::copied::{slot_to_pci_path, SlotType}; #[derive(Debug, Eq, PartialEq, Error)] pub enum Error { @@ -51,44 +39,23 @@ pub struct InstanceContext { pub state: api::InstanceState, pub generation: u64, pub properties: api::InstanceProperties, - serial: Arc>, - serial_task: SerialTask, + serial: Arc, + serial_task: serial::SerialTask, state_watcher_rx: watch::Receiver, state_watcher_tx: watch::Sender, } impl InstanceContext { - pub fn new(properties: api::InstanceProperties, log: &Logger) -> Self { + pub fn new(properties: api::InstanceProperties, _log: &Logger) -> Self { let (state_watcher_tx, state_watcher_rx) = watch::channel(api::InstanceStateMonitorResponse { gen: 0, state: api::InstanceState::Creating, migration: None, }); - let mock_uart = Arc::new(MockUart::new(&properties.name)); - let sink_size = NonZeroUsize::new(64).unwrap(); - let source_size = NonZeroUsize::new(1024).unwrap(); - let serial = Arc::new(Serial::new(mock_uart, sink_size, source_size)); - let serial_clone = serial.clone(); - - let (websocks_ch, websocks_recv) = mpsc::channel(1); - let (control_ch, control_recv) = mpsc::channel(1); - - let log = log.new(slog::o!("component" => "serial task")); - let task = tokio::spawn(async move { - if let Err(e) = super::serial::instance_serial_task( - websocks_recv, - control_recv, - serial_clone, - log.clone(), - ) - .await - { - error!(log, "Spawning serial task failed: {}", e); - } - }); + let serial = serial::Serial::new(&properties.name); - let serial_task = SerialTask { task, control_ch, websocks_ch }; + let serial_task = serial::SerialTask::spawn(); Self { state: api::InstanceState::Creating, @@ -104,7 +71,7 @@ impl InstanceContext { /// Updates the state of the mock instance. /// /// Returns an error if the state transition is invalid. - pub fn set_target_state( + pub async fn set_target_state( &mut self, target: api::InstanceStateRequested, ) -> Result<(), Error> { @@ -137,6 +104,7 @@ impl InstanceContext { } api::InstanceStateRequested::Stop => { self.state = api::InstanceState::Stopped; + self.serial_task.shutdown().await; Ok(()) } }, @@ -147,13 +115,12 @@ impl InstanceContext { /// Contextual information accessible from mock HTTP callbacks. pub struct Context { instance: Mutex>, - _config: Config, log: Logger, } impl Context { - pub fn new(config: Config, log: Logger) -> Self { - Context { instance: Mutex::new(None), _config: config, log } + pub fn new(log: Logger) -> Self { + Context { instance: Mutex::new(None), log } } } @@ -313,7 +280,7 @@ async fn instance_state_put( ) })?; let requested_state = request.into_inner(); - instance.set_target_state(requested_state).map_err(|err| { + instance.set_target_state(requested_state).await.map_err(|err| { HttpError::for_internal_error(format!("Failed to transition: {}", err)) })?; Ok(HttpResponseUpdatedNoContent {}) @@ -351,23 +318,23 @@ async fn instance_serial( Some(instance_ctx) => { let serial = instance_ctx.serial.clone(); - let byte_offset = - SerialHistoryOffset::try_from(&query.into_inner()).ok(); - if let Some(mut byte_offset) = byte_offset { + let query_params = query.into_inner(); + let history_query = serial::HistoryQuery::from_query( + query_params.from_start, + query_params.most_recent, + ); + if let Some(mut hq) = history_query { loop { - let (data, offset) = - serial.history_vec(byte_offset, None).await?; + let (data, offset) = serial.history_vec(hq, None).await?; if data.is_empty() { break; } ws_stream.send(Message::Binary(data)).await?; - byte_offset = SerialHistoryOffset::FromStart(offset); + hq = serial::HistoryQuery::FromStart(offset); } } - - instance_ctx.serial_task.websocks_ch.send(ws_stream).await.map_err( - |e| format!("Serial socket hand-off failed: {}", e).into(), - ) + instance_ctx.serial_task.new_conn(ws_stream).await; + Ok(()) } } } @@ -382,7 +349,18 @@ async fn instance_serial_history_get( ) -> Result, HttpError> { let query_params = query.into_inner(); - let byte_offset = SerialHistoryOffset::try_from(&query_params)?; + + let history_query = serial::HistoryQuery::from_query( + query_params.from_start, + query_params.most_recent, + ) + .ok_or_else(|| { + HttpError::for_bad_request( + None, + "Exactly one of 'from_start' or 'most_recent' must be specified." + .to_string(), + ) + })?; let max_bytes = query_params.max_bytes.map(|x| x as usize); let ctx = rqctx.context(); @@ -395,7 +373,7 @@ async fn instance_serial_history_get( "No mock instance instantiated".to_string(), ))? .serial - .history_vec(byte_offset, max_bytes) + .history_vec(history_query, max_bytes) .await .map_err(|e| HttpError::for_bad_request(None, e.to_string()))?; @@ -405,87 +383,265 @@ async fn instance_serial_history_get( })) } -// (ahem) mock *thou* art. -struct MockUart { - buf: std::sync::Mutex>, -} +mod serial { + use std::sync::atomic::{AtomicBool, Ordering}; + use std::sync::Arc; -impl MockUart { - fn new(name: &str) -> Self { - let mut buf = VecDeque::with_capacity(1024); - #[rustfmt::skip] - let gerunds = [ - "Loading", "Reloading", "Advancing", "Reticulating", "Defeating", - "Spoiling", "Cooking", "Destroying", "Resenting", "Introducing", - "Reiterating", "Blasting", "Tolling", "Delivering", "Engendering", - "Establishing", - ]; - #[rustfmt::skip] - let nouns = [ - "canon", "browsers", "meta", "splines", "villains", - "plot", "books", "evidence", "decisions", "chaos", - "points", "processors", "bells", "value", "gender", - "shots", - ]; - let mut hasher = std::collections::hash_map::DefaultHasher::new(); - name.hash(&mut hasher); - let mut entropy = hasher.finish(); - buf.extend( - format!( - "This is simulated serial console output for {}.\r\n", - name - ) - .as_bytes(), - ); - while entropy != 0 { - let gerund = gerunds[entropy as usize % gerunds.len()]; - entropy /= gerunds.len() as u64; - let noun = nouns[entropy as usize % nouns.len()]; - entropy /= nouns.len() as u64; - buf.extend( - format!( - "{} {}... {}[\x1b[92m 0K \x1b[m]\r\n", - gerund, - noun, - " ".repeat(40 - gerund.len() - noun.len()) - ) - .as_bytes(), - ); + use futures::StreamExt; + use hyper::upgrade::Upgraded; + use tokio::sync::{mpsc, Notify}; + use tokio_tungstenite::tungstenite::protocol::{ + frame::coding::CloseCode, CloseFrame, + }; + use tokio_tungstenite::WebSocketStream; + + type WsConn = WebSocketStream; + + const DEFAULT_MAX_LEN: usize = 1024; + + pub(crate) enum HistoryQuery { + FromStart(usize), + MostRecent(usize), + } + impl HistoryQuery { + pub(crate) const fn from_query( + from_start: Option, + most_recent: Option, + ) -> Option { + match (from_start, most_recent) { + (Some(from_start), None) => { + Some(Self::FromStart(from_start as usize)) + } + (None, Some(most_recent)) => { + Some(Self::MostRecent(most_recent as usize)) + } + _ => None, + } } - buf.extend( - format!( - "\x1b[2J\x1b[HOS/478 ({name}) (ttyl)\r\n\r\n{name} login: ", - name = name - ) - .as_bytes(), - ); - Self { buf: std::sync::Mutex::new(buf) } } -} -impl chardev::Sink for MockUart { - fn write(&self, data: u8) -> bool { - self.buf.lock().unwrap().push_back(data); - true + /// Fake serial task + pub(crate) struct SerialTask { + chan_ctrl: mpsc::Sender<()>, + chan_ws: mpsc::Sender, + is_shutdown: AtomicBool, } + impl SerialTask { + pub fn spawn() -> Self { + let (ctrl_send, ctrl_recv) = mpsc::channel(1); + let (ws_send, ws_recv) = mpsc::channel::(1); - fn set_notifier(&self, _f: Option) {} -} + tokio::spawn(async move { + Self::serial_task_work(ctrl_recv, ws_recv).await + }); + Self { + chan_ctrl: ctrl_send, + chan_ws: ws_send, + is_shutdown: AtomicBool::new(false), + } + } + + /// Drive client connections to the UART websocket + /// + /// At this time, there is no real data being emitted from the mock + /// instance besides what's made up in the [`Serial`] below. Because of + /// that, the serial task has little to do besides holding the websocket + /// connections open until the mock instance enters shutdown. + async fn serial_task_work( + mut chan_ctrl: mpsc::Receiver<()>, + mut chan_ws: mpsc::Receiver, + ) { + let bail = Notify::new(); + let mut connections = futures::stream::FuturesUnordered::new(); + let mut is_shutdown = false; + + /// Send appropriate shutdown notice + async fn close_for_shutdown(mut conn: WsConn) { + let _ = conn + .close(Some(CloseFrame { + code: CloseCode::Away, + reason: "VM stopped".into(), + })) + .await; + } -impl chardev::Source for MockUart { - fn read(&self) -> Option { - self.buf.lock().unwrap().pop_front() + /// Wait for a client connection to close (while discarding any + /// input from it), or a signal that the VM is shutting down. + async fn wait_for_close( + mut conn: WsConn, + bail: &Notify, + ) -> Option { + let mut pconn = std::pin::Pin::new(&mut conn); + + loop { + tokio::select! { + msg = pconn.next() => { + // Discard input (if any) and keep truckin' + msg.as_ref()?; + }, + _ = bail.notified() => { + return Some(conn); + } + } + } + } + + loop { + tokio::select! { + _vm_shutdown = chan_ctrl.recv() => { + // We've been signaled that the VM is shutdown + bail.notify_waiters(); + chan_ws.close(); + is_shutdown = true; + if connections.is_empty() { + return; + } + } + conn = chan_ws.recv() => { + // A new client connection has been passed to us + if conn.is_none() { + continue; + } + let conn = conn.unwrap(); + if is_shutdown { + close_for_shutdown(conn).await; + continue; + } + connections + .push(async { wait_for_close(conn, &bail).await }); + } + disconnect = connections.next(), if !connections.is_empty() => { + match disconnect { + None => { + // last open client + assert!(connections.is_empty()); + if is_shutdown { + return; + } + } + Some(Some(conn)) => { + // client needs disconnect due to shutdown + close_for_shutdown(conn).await; + } + _ => { + // client disconnected itself + continue + } + } + } + } + } + } + + pub async fn new_conn(&self, ws: WsConn) { + if let Err(mut ws) = self.chan_ws.send(ws).await.map_err(|e| e.0) { + let _ = ws + .close(Some(CloseFrame { + code: CloseCode::Away, + reason: "VM stopped".into(), + })) + .await; + } + } + + pub async fn shutdown(&self) { + if !self.is_shutdown.swap(true, Ordering::Relaxed) { + self.chan_ctrl.send(()).await.unwrap(); + } + } } - fn discard(&self, count: usize) -> usize { - let mut buf = self.buf.lock().unwrap(); - let end = buf.len().min(count); - buf.drain(0..end).count() + /// Mock source of UART data from the guest, including history + pub(crate) struct Serial { + mock_data: Vec, } + impl Serial { + pub(super) fn new(name: &str) -> Arc { + Arc::new(Self { mock_data: Self::mock_data(name) }) + } - fn set_autodiscard(&self, _active: bool) {} + // Conjure up some fake console output + fn mock_data(name: &str) -> Vec { + use std::collections::hash_map::DefaultHasher; + use std::hash::{Hash, Hasher}; + + let mut buf = Vec::with_capacity(1024); + #[rustfmt::skip] + let gerunds = [ + "Loading", "Reloading", "Advancing", "Reticulating", + "Defeating", "Spoiling", "Cooking", "Destroying", "Resenting", + "Introducing", "Reiterating", "Blasting", "Tolling", + "Delivering", "Engendering", "Establishing", + ]; + #[rustfmt::skip] + let nouns = [ + "canon", "browsers", "meta", "splines", "villains", "plot", + "books", "evidence", "decisions", "chaos", "points", + "processors", "bells", "value", "gender", "shots", + ]; + let mut hasher = DefaultHasher::new(); + name.hash(&mut hasher); + let mut entropy = hasher.finish(); + buf.extend( + format!( + "This is simulated serial console output for {}.\r\n", + name + ) + .as_bytes(), + ); + while entropy != 0 { + let gerund = gerunds[entropy as usize % gerunds.len()]; + entropy /= gerunds.len() as u64; + let noun = nouns[entropy as usize % nouns.len()]; + entropy /= nouns.len() as u64; + buf.extend( + format!( + "{} {}... {}[\x1b[92m 0K \x1b[m]\r\n", + gerund, + noun, + " ".repeat(40 - gerund.len() - noun.len()) + ) + .as_bytes(), + ); + } + buf.extend( + format!( + "\x1b[2J\x1b[HOS/478 ({name}) (ttyl)\r\n\r\n{name} login: ", + name = name + ) + .as_bytes(), + ); + buf + } - fn set_notifier(&self, _f: Option) {} + pub async fn history_vec( + &self, + query: HistoryQuery, + max_bytes: Option, + ) -> Result<(Vec, usize), &'static str> { + let end = self.mock_data.len(); + let byte_limit = max_bytes.unwrap_or(DEFAULT_MAX_LEN); + + match query { + HistoryQuery::FromStart(n) => { + if n > self.mock_data.len() { + Err("requesting data beyond history") + } else { + let data = &self.mock_data[n..]; + let truncated = &data[..(data.len().min(byte_limit))]; + Ok((truncated.to_vec(), end)) + } + } + HistoryQuery::MostRecent(n) => { + let clamped = n.min(self.mock_data.len()); + let data = + &self.mock_data[(self.mock_data.len() - clamped)..]; + let truncated = &data[..(data.len().min(byte_limit))]; + Ok((truncated.to_vec(), end)) + } + } + } + } } /// Returns a Dropshot [`ApiDescription`] object to launch a mock Propolis @@ -500,59 +656,3 @@ pub fn api() -> ApiDescription> { api.register(instance_serial_history_get).unwrap(); api } - -#[cfg(test)] -mod tests { - use super::MockUart; - use crate::serial::history_buffer::SerialHistoryOffset; - use crate::serial::Serial; - use std::num::NonZeroUsize; - use std::sync::Arc; - - async fn read_at_least( - serial: &Arc>, - length: usize, - ) -> Vec { - let mut buf = [0; 1024]; - let mut data = Vec::::new(); - while let Some(count) = serial.read_source(&mut buf).await { - if count == 0 { - break; - } - data.extend(&buf[..count]); - if data.len() >= length { - break; - } - } - data - } - - async fn read_and_expect(serial: &Arc>, expected: &[u8]) { - let actual = read_at_least(serial, expected.len()).await; - assert_eq!(&actual[..expected.len()], expected); - } - - #[tokio::test] - async fn mock_uart() { - let mock_uart = Arc::new(MockUart::new("unit-test")); - let sink_size = NonZeroUsize::new(64).unwrap(); - let source_size = NonZeroUsize::new(1024).unwrap(); - let serial = Arc::new(Serial::new(mock_uart, sink_size, source_size)); - - let expected = - "This is simulated serial console output for unit-test".as_bytes(); - read_and_expect(&serial, expected).await; - - // check that history ranges work after read_source - let expected = "simulated serial console output for unit-test"; - let (data, end) = serial - .history_vec( - SerialHistoryOffset::FromStart(8), - Some(expected.len()), - ) - .await - .unwrap(); - assert_eq!(&data, expected.as_bytes()); - assert_eq!(end, 8 + data.len()); - } -} diff --git a/bin/mock-server/src/main.rs b/bin/mock-server/src/main.rs new file mode 100644 index 000000000..fe52c527a --- /dev/null +++ b/bin/mock-server/src/main.rs @@ -0,0 +1,122 @@ +// 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/. + +use std::net::SocketAddr; +use std::path::PathBuf; +use std::sync::Arc; + +use anyhow::anyhow; +use clap::Parser; +use dropshot::{ConfigDropshot, HandlerTaskMode, HttpServerStarter}; +use slog::{info, Drain}; + +#[derive(Debug, Parser)] +#[clap(about, version)] +/// An HTTP server providing access to Propolis +enum Args { + /// Generates the OpenAPI specification. + OpenApi, + /// Runs the Propolis server. + Run { + #[clap(action)] + cfg: PathBuf, + + #[clap(name = "PROPOLIS_IP:PORT", action)] + propolis_addr: SocketAddr, + + /// IP:Port for the Oximeter register address + #[clap(long, action)] + metric_addr: Option, + }, +} + +fn build_logger() -> slog::Logger { + let main_drain = if atty::is(atty::Stream::Stdout) { + let decorator = slog_term::TermDecorator::new().build(); + let drain = slog_term::FullFormat::new(decorator).build().fuse(); + slog_async::Async::new(drain) + .overflow_strategy(slog_async::OverflowStrategy::Block) + .build_no_guard() + } else { + let drain = + slog_bunyan::with_name("propolis-server", std::io::stdout()) + .build() + .fuse(); + slog_async::Async::new(drain) + .overflow_strategy(slog_async::OverflowStrategy::Block) + .build_no_guard() + }; + + let (dtrace_drain, probe_reg) = slog_dtrace::Dtrace::new(); + + let filtered_main = slog::LevelFilter::new(main_drain, slog::Level::Info); + + let log = slog::Logger::root( + slog::Duplicate::new(filtered_main.fuse(), dtrace_drain.fuse()).fuse(), + slog::o!(), + ); + + if let slog_dtrace::ProbeRegistration::Failed(err) = probe_reg { + slog::error!(&log, "Error registering slog-dtrace probes: {:?}", err); + } + + log +} + +pub fn run_openapi() -> Result<(), String> { + propolis_mock_server::api() + .openapi("Oxide Propolis Server API", "0.0.1") + .description( + "API for interacting with the Propolis hypervisor frontend.", + ) + .contact_url("https://oxide.computer") + .contact_email("api@oxide.computer") + .write(&mut std::io::stdout()) + .map_err(|e| e.to_string()) +} + +async fn run_server( + config_dropshot: dropshot::ConfigDropshot, + _metrics_addr: Option, + log: slog::Logger, +) -> anyhow::Result<()> { + let context = propolis_mock_server::Context::new(log.new(slog::o!())); + + info!(log, "Starting server..."); + + let server = HttpServerStarter::new( + &config_dropshot, + propolis_mock_server::api(), + Arc::new(context), + &log, + ) + .map_err(|error| anyhow!("Failed to start server: {}", error))? + .start(); + + let server_res = server.await; + server_res.map_err(|e| anyhow!("Server exited with an error: {}", e)) +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // Command line arguments. + let args = Args::parse(); + + match args { + Args::OpenApi => run_openapi() + .map_err(|e| anyhow!("Cannot generate OpenAPI spec: {}", e)), + Args::Run { cfg: _cfg, propolis_addr, metric_addr } => { + // Dropshot configuration. + let config_dropshot = ConfigDropshot { + bind_address: propolis_addr, + request_body_max_bytes: 1024 * 1024, // 1M for ISO bytes + default_handler_task_mode: HandlerTaskMode::Detached, + }; + + let log = build_logger(); + + run_server(config_dropshot, metric_addr, log).await + } + } +} diff --git a/bin/propolis-server/Cargo.toml b/bin/propolis-server/Cargo.toml index cce239419..8b1d5999f 100644 --- a/bin/propolis-server/Cargo.toml +++ b/bin/propolis-server/Cargo.toml @@ -23,7 +23,6 @@ async-trait.workspace = true bit_field.workspace = true bitvec.workspace = true bytes.workspace = true -cfg-if.workspace = true chrono = { workspace = true, features = [ "serde" ] } clap = { workspace = true, features = ["derive"] } const_format.workspace = true @@ -78,9 +77,5 @@ default = [] # (nominally an Omicron package), certain code is compiled in or out. omicron-build = ["propolis/omicron-build"] -# If selected, only build a mock server which does not actually spawn instances -# (i.e. to test with a facsimile of the API on unsupported platforms) -mock-only = [] - # Falcon builds require corresponding bits turned on in the dependency libs falcon = ["propolis/falcon", "propolis-client/falcon"] diff --git a/bin/propolis-server/src/lib/lib.rs b/bin/propolis-server/src/lib/lib.rs index fe5e77b18..cf8e79b15 100644 --- a/bin/propolis-server/src/lib/lib.rs +++ b/bin/propolis-server/src/lib/lib.rs @@ -2,22 +2,13 @@ // 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/. -// always present pub mod config; -#[cfg(any(test, feature = "mock-only"))] -pub mod mock_server; +mod initializer; +mod migrate; mod serial; -#[cfg_attr(feature = "mock-only", allow(unused))] +pub mod server; mod spec; - -cfg_if::cfg_if! { - if #[cfg(not(feature = "mock-only"))] { - mod initializer; - mod migrate; - pub mod server; - mod stats; - mod vcpu_tasks; - mod vm; - pub mod vnc; - } -} +mod stats; +mod vcpu_tasks; +mod vm; +pub mod vnc; diff --git a/bin/propolis-server/src/lib/serial/mod.rs b/bin/propolis-server/src/lib/serial/mod.rs index 018da3159..c0904be31 100644 --- a/bin/propolis-server/src/lib/serial/mod.rs +++ b/bin/propolis-server/src/lib/serial/mod.rs @@ -4,8 +4,6 @@ //! Routines to expose a connection to an instance's serial port. -#![cfg_attr(feature = "mock-only", allow(unused))] -#[cfg(not(feature = "mock-only"))] use crate::migrate::MigrateError; use std::collections::HashMap; @@ -357,7 +355,6 @@ impl Serial { self.task_control_ch.lock().await.replace(control_ch); } - #[cfg(not(feature = "mock-only"))] pub(crate) async fn export_history( &self, destination: SocketAddr, @@ -378,7 +375,6 @@ impl Serial { Ok(encoded) } - #[cfg(not(feature = "mock-only"))] pub(crate) async fn import( &self, serialized_hist: &str, diff --git a/bin/propolis-server/src/main.rs b/bin/propolis-server/src/main.rs index e215049fb..52efc5aa1 100644 --- a/bin/propolis-server/src/main.rs +++ b/bin/propolis-server/src/main.rs @@ -2,8 +2,6 @@ // 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/. -#![cfg_attr(feature = "mock-only", allow(unused))] - use anyhow::{anyhow, Context}; use clap::Parser; use dropshot::{ConfigDropshot, HandlerTaskMode, HttpServerStarter}; @@ -14,17 +12,11 @@ use std::net::{IpAddr, Ipv6Addr, SocketAddr}; use std::path::PathBuf; use std::sync::Arc; -#[cfg(feature = "mock-only")] -use propolis_server::mock_server as server; - -cfg_if::cfg_if! { - if #[cfg(not(feature = "mock-only"))] { - use propolis_server::server::{self, MetricsEndpointConfig}; - use propolis_server::vnc::setup_vnc; - } -} - -use propolis_server::config; +use propolis_server::{ + config, + server::{self, MetricsEndpointConfig}, + vnc::setup_vnc, +}; #[derive(Debug, Parser)] #[clap(about, version)] @@ -65,7 +57,6 @@ pub fn run_openapi() -> Result<(), String> { .map_err(|e| e.to_string()) } -#[cfg(not(feature = "mock-only"))] async fn run_server( config_app: config::Config, config_dropshot: dropshot::ConfigDropshot, @@ -121,31 +112,6 @@ async fn run_server( server_res.map_err(|e| anyhow!("Server exited with an error: {}", e)) } -#[cfg(feature = "mock-only")] -async fn run_server( - config_app: config::Config, - config_dropshot: dropshot::ConfigDropshot, - _metrics_addr: Option, - _vnc_addr: SocketAddr, - log: slog::Logger, -) -> anyhow::Result<()> { - let context = server::Context::new(config_app, log.new(slog::o!())); - - info!(log, "Starting server..."); - - let server = HttpServerStarter::new( - &config_dropshot, - server::api(), - Arc::new(context), - &log, - ) - .map_err(|error| anyhow!("Failed to start server: {}", error))? - .start(); - - let server_res = server.await; - server_res.map_err(|e| anyhow!("Server exited with an error: {}", e)) -} - fn build_logger() -> slog::Logger { use slog::Drain; diff --git a/xtask/src/task_clippy.rs b/xtask/src/task_clippy.rs index 88a942373..3891faee9 100644 --- a/xtask/src/task_clippy.rs +++ b/xtask/src/task_clippy.rs @@ -36,8 +36,7 @@ pub(crate) fn cmd_clippy(strict: bool) -> Result<()> { failed |= run_clippy(&["-p", "propolis-server", "--features", "falcon"])?; // Check the mock server - failed |= - run_clippy(&["-p", "propolis-server", "--features", "mock-only"])?; + failed |= run_clippy(&["-p", "propolis-mock-server"])?; // Check standalone with crucible enabled failed |=