Skip to content
Merged
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
262 changes: 229 additions & 33 deletions Cargo.lock

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

members = [
"adm",
"dropshot-apis",
"lldpd-api",
"lldpd-client",
"lldpd-common",
Expand Down Expand Up @@ -30,7 +31,9 @@ anyhow = "1.0"
camino = { version = "1.1", features = ["serde1"] }
chrono = "0.4"
clap = { version = "4.5.45", features = ["derive"] }
dropshot = "0.16.3"
dropshot = "0.16.4"
dropshot-api-manager = "0.2.2"
dropshot-api-manager-types = "0.2.2"
futures = "0.3"
http = "0.2.9"
omicron-zone-package = "0.11.1"
Expand Down
14 changes: 14 additions & 0 deletions dropshot-apis/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "lldp-dropshot-apis"
version = "0.1.0"
edition = "2024"
license = "MPL-2.0"

[dependencies]
anyhow.workspace = true
camino.workspace = true
clap.workspace = true
lldpd-api.workspace = true
dropshot-api-manager-types.workspace = true
dropshot-api-manager.workspace = true
semver.workspace = true
78 changes: 78 additions & 0 deletions dropshot-apis/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// 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::process::ExitCode;

use anyhow::Context;
use camino::Utf8PathBuf;
use clap::Parser;
use dropshot_api_manager::{Environment, ManagedApiConfig, ManagedApis};
use dropshot_api_manager_types::{ManagedApiMetadata, Versions};
use lldpd_api::*;

pub fn environment() -> anyhow::Result<Environment> {
// The workspace root is one level up from this crate's directory.
let workspace_root = Utf8PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.to_path_buf();
let env = Environment::new(
// This is the command used to run the OpenAPI manager.
"cargo xtask openapi".to_owned(),
workspace_root,
// This is the location within the workspace root where the OpenAPI
// documents are stored.
"openapi",
)?;
Ok(env)
}

/// The list of APIs managed by the OpenAPI manager.
pub fn all_apis() -> anyhow::Result<ManagedApis> {
let apis = vec![ManagedApiConfig {
ident: "lldpd",
versions: Versions::Lockstep {
version: semver::Version::new(0, 0, 1),
},
title: "Oxide LLDP Daemon",
metadata: ManagedApiMetadata {
description: Some("API for managing the LLDP daemon"),
contact_url: Some("https://oxide.computer"),
contact_email: Some("api@oxide.computer"),
..Default::default()
},
api_description: lldpd_api_mod::stub_api_description,
extra_validation: None,
}];

let apis = ManagedApis::new(apis).context("error creating ManagedApis")?;
Ok(apis)
}

fn main() -> anyhow::Result<ExitCode> {
let app = dropshot_api_manager::App::parse();
let env = environment()?;
let apis = all_apis()?;

Ok(app.exec(&env, &apis))
}

#[cfg(test)]
mod test {
use dropshot_api_manager::test_util::check_apis_up_to_date;

use super::*;

// Also recommended: a test which ensures documents are up-to-date. The
// OpenAPI manager comes with a helper function for this, called
// `check_apis_up_to_date`.
#[test]
fn test_apis_up_to_date() -> anyhow::Result<ExitCode> {
let env = environment()?;
let apis = all_apis()?;

let result = check_apis_up_to_date(&env, &apis)?;
Ok(result.to_exit_code())
}
}
2 changes: 1 addition & 1 deletion lldpd-types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ license = "MPL-2.0"
[dependencies]
chrono.workspace = true
protocol.workspace = true
schemars.workspace = true
schemars = { workspace = true, features = ["chrono"] }
serde.workspace = true
uuid.workspace = true
8 changes: 4 additions & 4 deletions lldpd/src/api_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,10 @@ pub async fn api_server_manager(
}
}

pub fn http_api() -> dropshot::ApiDescription<Arc<Global>> {
lldpd_api_mod::api_description::<LldpdApiImpl>().unwrap()
}

#[cfg(test)]
mod tests {
use crate::api_server::build_info;
Expand All @@ -672,7 +676,3 @@ mod tests {
assert_eq!(info.git_sha, ours);
}
}

pub fn http_api() -> dropshot::ApiDescription<Arc<Global>> {
lldpd_api_mod::api_description::<LldpdApiImpl>().unwrap()
}
14 changes: 0 additions & 14 deletions lldpd/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,6 @@ pub struct SwitchInfo {
enum Args {
/// Run the LLDPD API server.
Run(Opt),
Copy link
Collaborator

Choose a reason for hiding this comment

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

If there is only a single subcommand, we can probably just take it out entirely.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

/// Generate an OpenAPI specification for the LLDPD server.
Openapi,
}

#[derive(Debug, StructOpt)]
Expand Down Expand Up @@ -246,23 +244,11 @@ async fn run_lldpd(opts: Opt) -> LldpdResult<()> {
Ok(())
}

fn print_openapi() -> LldpdResult<()> {
lldpd_api::lldpd_api_mod::stub_api_description()
.unwrap()
.openapi("Oxide LLDP Daemon", "0.0.1".parse().unwrap())
.description("API for managing the LLDP daemon")
.contact_url("https://oxide.computer")
.contact_email("api@oxide.computer")
.write(&mut std::io::stdout())
.map_err(|e| LldpdError::Io(e.into()))
}

#[tokio::main(flavor = "multi_thread")]
async fn main() -> LldpdResult<()> {
let args = Args::from_args();

match args {
Args::Openapi => print_openapi(),
Args::Run(opt) => run_lldpd(opt).await,
}
}
131 changes: 131 additions & 0 deletions xtask/src/external.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// 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/.

//! External xtasks. (extasks?)

use std::ffi::OsString;
use std::os::unix::process::CommandExt;
use std::process::Command;

use anyhow::{Context, Result};
use clap::Parser;

/// Argument parser for external xtasks.
///
/// In general we want all developer tasks to be discoverable simply by running
/// `cargo xtask`, but some development tools end up with a particularly
/// large dependency tree. It's not ideal to have to pay the cost of building
/// our release engineering tooling if all the user wants to do is check for
/// workspace dependency issues.
///
/// `External` provides a pattern for creating xtasks that live in other crates.
/// An external xtask is defined on `crate::Cmds` as a tuple variant containing
/// `External`, which captures all arguments and options (even `--help`) as
/// a `Vec<OsString>`. The main function then calls `External::exec` with the
/// appropriate bin target name and any additional Cargo arguments.
#[derive(Debug, Parser)]
#[clap(
disable_help_flag(true),
disable_help_subcommand(true),
disable_version_flag(true)
)]
pub struct External {
#[clap(trailing_var_arg(true), allow_hyphen_values(true))]
args: Vec<OsString>,

// This stores an in-progress Command builder. `cargo_args` appends args
// to it, and `exec` consumes it. Clap does not treat this as a command
// (`skip`), but fills in this field by calling `new_command`.
#[clap(skip = new_command())]
command: Command,
}

impl External {
pub fn exec_bin(
self,
package: impl AsRef<str>,
bin_target: impl AsRef<str>,
) -> Result<()> {
self.exec_common(&[
"--package",
package.as_ref(),
"--bin",
bin_target.as_ref(),
])
}

fn exec_common(mut self, args: &[&str]) -> Result<()> {
let error = self.command.args(args).arg("--").args(self.args).exec();
Err(error).context("failed to exec `cargo run`")
}
}

fn new_command() -> Command {
let mut command = cargo_command(CargoLocation::FromEnv);
command.arg("run");
command
}

/// Creates and prepares a `std::process::Command` for the `cargo` executable.
pub fn cargo_command(location: CargoLocation) -> Command {
let mut command = location.resolve();

for (key, _) in std::env::vars_os() {
let Some(key) = key.to_str() else { continue };
if SANITIZED_ENV_VARS.matches(key) {
command.env_remove(key);
}
}

command
}

/// How to determine the location of the `cargo` executable.
#[derive(Clone, Copy, Debug)]
pub enum CargoLocation {
/// Use the `CARGO` environment variable, and fall back to `"cargo"` if it
/// is not set.
FromEnv,
}

impl CargoLocation {
fn resolve(self) -> Command {
match self {
CargoLocation::FromEnv => {
let cargo = std::env::var_os("CARGO")
.unwrap_or_else(|| OsString::from("cargo"));
Command::new(&cargo)
}
}
}
}

#[derive(Debug)]
struct SanitizedEnvVars {
// At the moment we only ban some prefixes, but we may also want to ban env
// vars by exact name in the future.
prefixes: &'static [&'static str],
}

impl SanitizedEnvVars {
const fn new() -> Self {
// Remove many of the environment variables set in
// https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-build-scripts.
// This is done to avoid recompilation with crates like ring between
// `cargo clippy` and `cargo xtask clippy`. (This is really a bug in
// both ring's build script and in Cargo.)
//
// The current list is informed by looking at ring's build script, so
// it's not guaranteed to be exhaustive and it may need to grow over
// time.
let prefixes = &["CARGO_PKG_", "CARGO_MANIFEST_", "CARGO_CFG_"];
Self { prefixes }
}

fn matches(&self, key: &str) -> bool {
self.prefixes.iter().any(|prefix| key.starts_with(prefix))
}
}

static SANITIZED_ENV_VARS: SanitizedEnvVars = SanitizedEnvVars::new();
7 changes: 7 additions & 0 deletions xtask/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ use std::path::Path;
use anyhow::{anyhow, Context, Result};
use clap::{Parser, ValueEnum};

mod external;

#[cfg(target_os = "illumos")]
mod illumos;
#[cfg(target_os = "illumos")]
Expand Down Expand Up @@ -41,6 +43,8 @@ pub enum DistFormat {
/// lldp xtask support
#[clap(name = "xtask")]
enum Xtasks {
/// manage OpenAPI documents
Openapi(Box<external::External>),
/// build an installable dataplane controller package
Dist {
/// package release bits
Expand Down Expand Up @@ -95,6 +99,9 @@ fn collect_binaries(release: bool, dst: &str) -> Result<()> {
async fn main() {
let task = Xtasks::parse();
if let Err(e) = match task {
Xtasks::Openapi(external) => {
external.exec_bin("lldp-dropshot-apis", "lldp-dropshot-apis")
}
Xtasks::Dist { release, format } => plat::dist(release, format).await,
} {
eprintln!("failed: {e}");
Expand Down