From 283589670fd1543a7aef38c15bb58a01790ff34a Mon Sep 17 00:00:00 2001 From: iliana etaoin Date: Fri, 19 Nov 2021 21:58:38 +0000 Subject: [PATCH 01/14] initial work for using the tough client in nexus --- Cargo.lock | 126 +++++++++++++++++++++ common/src/api/external/mod.rs | 3 + common/src/sql/dbinit.sql | 31 ++++- nexus/Cargo.toml | 1 + nexus/src/config.rs | 2 + nexus/src/context.rs | 7 +- nexus/src/db/datastore.rs | 28 ++++- nexus/src/db/model.rs | 41 ++++++- nexus/src/db/schema.rs | 15 +++ nexus/src/external_api/http_entrypoints.rs | 24 ++++ nexus/src/lib.rs | 7 +- nexus/src/nexus.rs | 92 ++++++++++++++- nexus/src/updates.rs | 23 ++++ 13 files changed, 384 insertions(+), 16 deletions(-) create mode 100644 nexus/src/updates.rs diff --git a/Cargo.lock b/Cargo.lock index 42edb9666cf..7aef5c4070f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -267,6 +267,15 @@ dependencies = [ "byte-tools", ] +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "memchr", +] + [[package]] name = "bumpalo" version = "3.7.1" @@ -722,6 +731,12 @@ dependencies = [ "num_enum", ] +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + [[package]] name = "dof" version = "0.1.5" @@ -1112,6 +1127,19 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +[[package]] +name = "globset" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10463d9ff00a2a068db14231982f5132edebad0d7660cd956a1c30292dbcbfbd" +dependencies = [ + "aho-corasick", + "bstr", + "fnv", + "log", + "regex", +] + [[package]] name = "group" version = "0.10.0" @@ -1716,6 +1744,17 @@ dependencies = [ "syn", ] +[[package]] +name = "olpc-cjson" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ca49fe685014bbf124ee547da94ed7bb65a6eb9dc9c4711773c081af96a39c" +dependencies = [ + "serde", + "serde_json", + "unicode-normalization", +] + [[package]] name = "omicron-common" version = "0.1.0" @@ -1817,6 +1856,7 @@ dependencies = [ "tokio", "tokio-postgres", "toml", + "tough", "uuid", ] @@ -2190,6 +2230,35 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0744126afe1a6dd7f394cb50a716dbe086cb06e255e53d8d0185d82828358fb5" +[[package]] +name = "path-absolutize" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b288298a7a3a7b42539e3181ba590d32f2d91237b0691ed5f103875c754b3bf5" +dependencies = [ + "path-dedot", +] + +[[package]] +name = "path-dedot" +version = "3.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bfa72956f6be8524f7f7e2b07972dda393cb0008a6df4451f658b7e1bd1af80" +dependencies = [ + "once_cell", +] + +[[package]] +name = "pem" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06673860db84d02a63942fa69cd9543f2624a5df3aea7f33173048fa7ad5cf1a" +dependencies = [ + "base64", + "once_cell", + "regex", +] + [[package]] name = "percent-encoding" version = "2.1.0" @@ -3090,6 +3159,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_plain" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95455e7e29fada2052e72170af226fbe368a4ca33dee847875325d9fdb133858" +dependencies = [ + "serde", +] + [[package]] name = "serde_tokenstream" version = "0.1.2" @@ -3362,6 +3440,27 @@ dependencies = [ "thiserror", ] +[[package]] +name = "snafu" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eab12d3c261b2308b0d80c26fffb58d17eba81a4be97890101f416b478c79ca7" +dependencies = [ + "doc-comment", + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1508efa03c362e23817f96cde18abed596a25219a8b2c66e8db33c03543d315b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "socket2" version = "0.4.2" @@ -3906,6 +4005,33 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tough" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99488309ba53ee931b6ccda1cde07feaab95f214d328e3a7244c0f7563b5909f" +dependencies = [ + "chrono", + "dyn-clone", + "globset", + "hex", + "log", + "olpc-cjson", + "path-absolutize", + "pem", + "percent-encoding", + "reqwest", + "ring", + "serde", + "serde_json", + "serde_plain", + "snafu", + "tempfile", + "untrusted", + "url", + "walkdir", +] + [[package]] name = "tower-service" version = "0.3.1" diff --git a/common/src/api/external/mod.rs b/common/src/api/external/mod.rs index 03d4a9644d9..41d3e938f9a 100644 --- a/common/src/api/external/mod.rs +++ b/common/src/api/external/mod.rs @@ -484,6 +484,7 @@ pub enum ResourceType { Oximeter, MetricProducer, Zpool, + UpdateAvailableArtifact, } impl Display for ResourceType { @@ -509,6 +510,8 @@ impl Display for ResourceType { ResourceType::Oximeter => "oximeter", ResourceType::MetricProducer => "metric producer", ResourceType::Zpool => "zpool", + ResourceType::UpdateAvailableArtifact => + "available update artifact", } ) } diff --git a/common/src/sql/dbinit.sql b/common/src/sql/dbinit.sql index 2757b0f3d5b..aae2eeae6fe 100644 --- a/common/src/sql/dbinit.sql +++ b/common/src/sql/dbinit.sql @@ -46,7 +46,11 @@ CREATE TABLE omicron.public.rack ( /* Identity metadata (asset) */ id UUID PRIMARY KEY, time_created TIMESTAMPTZ NOT NULL, - time_modified TIMESTAMPTZ NOT NULL + time_modified TIMESTAMPTZ NOT NULL, + + /* Used to configure the updates service URLs */ + tuf_metadata_base_url STRING(512) NOT NULL, + tuf_targets_base_url STRING(512) NOT NULL ); /* @@ -644,6 +648,31 @@ CREATE INDEX ON omicron.public.console_session ( /*******************************************************************/ +CREATE TYPE omicron.public.update_artifact_kind AS ENUM ( + 'zone' +); + +CREATE TABLE omicron.public.update_available_artifact ( + name STRING(40) NOT NULL, + version INT NOT NULL, + kind omicron.public.update_artifact_kind NOT NULL, + + /* the version of the targets.json role this came from */ + targets_version INT NOT NULL, + + /* when the metadata this artifact was cached from expires */ + metadata_expiration TIMESTAMPTZ NOT NULL, + + /* data about the target from the targets.json role */ + target_name STRING(512) NOT NULL, + target_sha256 STRING(64) NOT NULL, + target_length INT NOT NULL, + + PRIMARY KEY (name, version, kind) +); + +/*******************************************************************/ + /* * Metadata for the schema itself. This version number isn't great, as there's * nothing to ensure it gets bumped when it should be, but it's a start. diff --git a/nexus/Cargo.toml b/nexus/Cargo.toml index 35ab9382942..6d75c74c76d 100644 --- a/nexus/Cargo.toml +++ b/nexus/Cargo.toml @@ -40,6 +40,7 @@ sled-agent-client = { path = "../sled-agent-client" } structopt = "0.3" thiserror = "1.0" toml = "0.5.6" +tough = { version = "0.12", features = [ "http" ] } [dependencies.api_identity] path = "../api_identity" diff --git a/nexus/src/config.rs b/nexus/src/config.rs index 602ba266466..2802fdf2adb 100644 --- a/nexus/src/config.rs +++ b/nexus/src/config.rs @@ -59,6 +59,8 @@ pub struct Config { pub database: db::Config, /** Authentication-related configuration */ pub authn: AuthnConfig, + + pub tuf_trusted_root: PathBuf, } #[derive(Debug)] diff --git a/nexus/src/context.rs b/nexus/src/context.rs index 0496b9bd22a..462659c3de0 100644 --- a/nexus/src/context.rs +++ b/nexus/src/context.rs @@ -69,8 +69,8 @@ impl ServerContext { * Create a new context with the given rack id and log. This creates the * underlying nexus as well. */ - pub fn new( - rack_id: &Uuid, + pub async fn new( + rack_id: Uuid, log: Logger, pool: db::Pool, config: &config::Config, @@ -140,7 +140,8 @@ impl ServerContext { pool, config, Arc::clone(&authz), - ), + ) + .await, log, external_authn, authz, diff --git a/nexus/src/db/datastore.rs b/nexus/src/db/datastore.rs index 7e20306592e..4b47fb17e95 100644 --- a/nexus/src/db/datastore.rs +++ b/nexus/src/db/datastore.rs @@ -59,9 +59,9 @@ use crate::db::{ ConsoleSession, Dataset, Disk, DiskAttachment, DiskRuntimeState, Generation, Instance, InstanceRuntimeState, Name, Organization, OrganizationUpdate, OximeterInfo, ProducerEndpoint, Project, - ProjectUpdate, RouterRoute, RouterRouteUpdate, Sled, Vpc, - VpcFirewallRule, VpcRouter, VpcRouterUpdate, VpcSubnet, - VpcSubnetUpdate, VpcUpdate, Zpool, + ProjectUpdate, RouterRoute, RouterRouteUpdate, Sled, + UpdateAvailableArtifact, Vpc, VpcFirewallRule, VpcRouter, + VpcRouterUpdate, VpcSubnet, VpcSubnetUpdate, VpcUpdate, Zpool, }, pagination::paginated, update_and_check::{UpdateAndCheck, UpdateStatus}, @@ -1938,6 +1938,28 @@ impl DataStore { )) }) } + + pub async fn update_available_artifact_upsert( + &self, + artifact: UpdateAvailableArtifact, + ) -> CreateResult { + use db::schema::update_available_artifact::dsl; + diesel::insert_into(dsl::update_available_artifact) + .values(artifact.clone()) + .on_conflict((dsl::name, dsl::version, dsl::kind)) + .do_update() + .set(artifact.clone()) + .returning(UpdateAvailableArtifact::as_returning()) + .get_result_async(self.pool()) + .await + .map_err(|e| { + public_error_from_diesel_pool_create( + e, + ResourceType::UpdateAvailableArtifact, + &artifact.to_string(), + ) + }) + } } #[cfg(test)] diff --git a/nexus/src/db/model.rs b/nexus/src/db/model.rs index a19dbbc216b..fb7412805b0 100644 --- a/nexus/src/db/model.rs +++ b/nexus/src/db/model.rs @@ -9,7 +9,8 @@ use crate::db::identity::{Asset, Resource}; use crate::db::schema::{ console_session, dataset, disk, instance, metric_producer, network_interface, organization, oximeter, project, rack, region, - router_route, sled, vpc, vpc_firewall_rule, vpc_router, vpc_subnet, zpool, + router_route, sled, update_available_artifact, vpc, vpc_firewall_rule, + vpc_router, vpc_subnet, zpool, }; use crate::external_api::params; use crate::internal_api; @@ -396,6 +397,9 @@ where pub struct Rack { #[diesel(embed)] pub identity: RackIdentity, + + pub tuf_metadata_base_url: String, + pub tuf_targets_base_url: String, } /// Database representation of a Sled. @@ -1769,3 +1773,38 @@ impl ConsoleSession { Self { token, user_id, time_last_used: now, time_created: now } } } + +impl_enum_type!( + #[derive(SqlType, Debug)] + #[postgres(type_name = "update_artifact_kind", type_schema = "public")] + pub struct UpdateArtifactKindEnum; + + #[derive(Clone, Debug, Display, AsExpression, FromSqlRow)] + #[display("{0}")] + #[sql_type = "UpdateArtifactKindEnum"] + pub struct UpdateArtifactKind(pub crate::updates::UpdateArtifactKind); + + // Enum values + Zone => b"zone" +); + +#[derive( + Queryable, Insertable, Clone, Debug, Display, Selectable, AsChangeset, +)] +#[table_name = "update_available_artifact"] +#[display("{kind} \"{name}\" v{version}")] +pub struct UpdateAvailableArtifact { + pub name: String, + /// Version of the artifact itself + pub version: i64, + pub kind: UpdateArtifactKind, + /// `version` field of targets.json from the repository + // FIXME this *should* be a NonZeroU64 + pub targets_version: i64, + pub metadata_expiration: DateTime, + pub target_name: String, + // FIXME should this be [u8; 32]? + pub target_sha256: String, + // FIXME this *should* be a u64 + pub target_length: i64, +} diff --git a/nexus/src/db/schema.rs b/nexus/src/db/schema.rs index 7d601807df1..b58769a23da 100644 --- a/nexus/src/db/schema.rs +++ b/nexus/src/db/schema.rs @@ -137,6 +137,8 @@ table! { id -> Uuid, time_created -> Timestamptz, time_modified -> Timestamptz, + tuf_metadata_base_url -> Text, + tuf_targets_base_url -> Text, } } @@ -289,6 +291,19 @@ table! { } } +table! { + update_available_artifact (name, version, kind) { + name -> Text, + version -> Int8, + kind -> crate::db::model::UpdateArtifactKindEnum, + targets_version -> Int8, + metadata_expiration -> Timestamptz, + target_name -> Text, + target_sha256 -> Text, + target_length -> Int8, + } +} + allow_tables_to_appear_in_same_query!( disk, instance, diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 97b82d31ef4..7974d450883 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -131,6 +131,8 @@ pub fn external_api() -> NexusApiDescription { api.register(hardware_sleds_get)?; api.register(hardware_sleds_get_sled)?; + api.register(updates_refresh)?; + api.register(sagas_get)?; api.register(sagas_get_saga)?; @@ -1868,6 +1870,28 @@ async fn hardware_sleds_get_sled( apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await } +/* + * Updates + */ + +/** + * Refresh update metadata + */ +#[endpoint { + method = POST, + path = "/updates/refresh", +}] +async fn updates_refresh( + rqctx: Arc>>, + // _query_params: Query, +) -> Result, HttpError> { + let apictx = rqctx.context(); + let nexus = &apictx.nexus; + let handler = + async { Ok(HttpResponseOk(nexus.updates_refresh_metadata().await?)) }; + apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await +} + /* * Sagas */ diff --git a/nexus/src/lib.rs b/nexus/src/lib.rs index aa070cd3adf..5ca59e79d94 100644 --- a/nexus/src/lib.rs +++ b/nexus/src/lib.rs @@ -26,6 +26,7 @@ pub mod internal_api; // public for testing mod nexus; mod saga_interface; mod sagas; +mod updates; pub use config::Config; pub use context::ServerContext; @@ -87,7 +88,7 @@ impl Server { */ pub async fn start( config: &Config, - rack_id: &Uuid, + rack_id: Uuid, log: &Logger, ) -> Result { let log = log.new(o!("name" => config.id.to_string())); @@ -95,7 +96,7 @@ impl Server { let ctxlog = log.new(o!("component" => "ServerContext")); let pool = db::Pool::new(&config.database); - let apictx = ServerContext::new(rack_id, ctxlog, pool, &config)?; + let apictx = ServerContext::new(rack_id, ctxlog, pool, &config).await?; let http_server_starter_external = dropshot::HttpServerStarter::new( &config.dropshot_external, @@ -168,7 +169,7 @@ pub async fn run_server(config: &Config) -> Result<(), String> { .to_logger("nexus") .map_err(|message| format!("initializing logger: {}", message))?; let rack_id = Uuid::new_v4(); - let server = Server::start(config, &rack_id, &log).await?; + let server = Server::start(config, rack_id, &log).await?; server.register_as_producer().await; server.wait_for_finish().await } diff --git a/nexus/src/nexus.rs b/nexus/src/nexus.rs index 79d8d6479cf..fe195e5a143 100644 --- a/nexus/src/nexus.rs +++ b/nexus/src/nexus.rs @@ -17,6 +17,7 @@ use crate::external_api::params; use crate::internal_api::params::{OximeterInfo, ZpoolPutRequest}; use crate::saga_interface::SagaContext; use crate::sagas; +use crate::updates::ArtifactsDocument; use anyhow::Context; use async_trait::async_trait; use futures::future::ready; @@ -127,6 +128,8 @@ pub struct Nexus { /** Task representing completion of recovered Sagas */ recovery_task: std::sync::Mutex>, + + tuf_trusted_root: Vec, } /* @@ -142,8 +145,8 @@ impl Nexus { * Create a new Nexus instance for the given rack id `rack_id` */ /* TODO-polish revisit rack metadata */ - pub fn new_with_id( - rack_id: &Uuid, + pub async fn new_with_id( + rack_id: Uuid, log: Logger, pool: db::Pool, config: &config::Config, @@ -166,12 +169,15 @@ impl Nexus { )); let nexus = Nexus { id: config.id, - rack_id: *rack_id, + rack_id, log: log.new(o!()), - api_rack_identity: db::model::RackIdentity::new(*rack_id), + api_rack_identity: db::model::RackIdentity::new(rack_id), db_datastore: Arc::clone(&db_datastore), sec_client: Arc::clone(&sec_client), recovery_task: std::sync::Mutex::new(None), + tuf_trusted_root: tokio::fs::read(&config.tuf_trusted_root) + .await + .unwrap(), }; /* TODO-cleanup all the extra Arcs here seems wrong */ @@ -1973,7 +1979,13 @@ impl Nexus { */ fn as_rack(&self) -> db::model::Rack { - db::model::Rack { identity: self.api_rack_identity.clone() } + db::model::Rack { + identity: self.api_rack_identity.clone(), + // FIXME your username is embedded in a path ya dingus + tuf_metadata_base_url: "file:///home/iliana/tuf/metadata" + .to_string(), + tuf_targets_base_url: "file:///home/iliana/tuf/targets".to_string(), + } } pub async fn racks_list( @@ -2249,6 +2261,76 @@ impl Nexus { pub async fn session_hard_delete(&self, token: String) -> DeleteResult { self.db_datastore.session_hard_delete(token).await } + + fn updates_load_artifacts( + &self, + ) -> Result< + Vec, + Box, + > { + // TODO(iliana): make async/.await. awslabs/tough#213 + use std::io::Read; + + let rack = self.as_rack(); + let repository = tough::RepositoryLoader::new( + self.tuf_trusted_root.as_slice(), + rack.tuf_metadata_base_url.parse()?, + rack.tuf_targets_base_url.parse()?, + ) + .load()?; + + let mut artifact_document = Vec::new(); + match repository.read_target(&"artifacts.json".parse()?)? { + Some(mut target) => target.read_to_end(&mut artifact_document)?, + None => return Err("artifacts.json missing".into()), + }; + let artifacts: ArtifactsDocument = + serde_json::from_slice(&artifact_document)?; + + let earliest_expiration = unimplemented!(); + + let mut v = Vec::new(); + for artifact in artifacts.artifacts { + if let Some(target) = repository + .targets() + .signed + .targets + .get(&artifact.target.parse()?) + { + v.push(db::model::UpdateAvailableArtifact { + name: artifact.name, + version: artifact.version, + kind: db::model::UpdateArtifactKind(artifact.kind), + targets_version: repository + .targets() + .signed + .version + .get() + .try_into()?, + metadata_expiration: earliest_expiration, + target_name: artifact.target, + target_sha256: hex::encode(&target.hashes.sha256), + target_length: target.length.try_into()?, + }); + } + } + Ok(v) + } + + pub async fn updates_refresh_metadata(&self) -> Result<(), Error> { + for artifact in self.updates_load_artifacts().map_err(|e| { + Error::InternalError { internal_message: e.to_string() } + })? { + self.db_datastore + .update_available_artifact_upsert(artifact) + .await?; + } + + // delete all rows remaining with a targets_version < repository.targets().version() + unimplemented!(); + + Ok(()) + } } fn generate_session_token() -> String { diff --git a/nexus/src/updates.rs b/nexus/src/updates.rs new file mode 100644 index 00000000000..d8463797245 --- /dev/null +++ b/nexus/src/updates.rs @@ -0,0 +1,23 @@ +use parse_display::Display; +use serde::Deserialize; + +// Schema for the `artifacts.json` target in the TUF update repository. +#[derive(Clone, Debug, Deserialize)] +pub struct ArtifactsDocument { + pub artifacts: Vec, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct UpdateArtifact { + pub name: String, + pub version: i64, + pub kind: UpdateArtifactKind, + pub target: String, +} + +#[derive(Clone, Debug, Display, Deserialize)] +#[display(style = "kebab-case")] +#[serde(rename_all = "kebab-case")] +pub enum UpdateArtifactKind { + Zone, +} From 052c7fd9ca7484c9c40b51c66b545b4b0a226cb5 Mon Sep 17 00:00:00 2001 From: iliana etaoin Date: Tue, 30 Nov 2021 22:36:14 +0000 Subject: [PATCH 02/14] run tough client in tokio::task::spawn_blocking nexus is async, tough is not async, but tough uses reqwest in blocking mode which is async. https://docs.rs/reqwest/0.11.7/reqwest/blocking/index.html: > Conversely, the functionality in `reqwest::blocking` must not be > executed within an async runtime, or it will panic when attempting to > block. moves the relevant code out to updates.rs, since the function can't borrow self due to lifetime constraints. --- nexus/src/nexus.rs | 75 +++++++++----------------------------------- nexus/src/updates.rs | 60 +++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 61 deletions(-) diff --git a/nexus/src/nexus.rs b/nexus/src/nexus.rs index fe195e5a143..5b15de015e1 100644 --- a/nexus/src/nexus.rs +++ b/nexus/src/nexus.rs @@ -17,7 +17,6 @@ use crate::external_api::params; use crate::internal_api::params::{OximeterInfo, ZpoolPutRequest}; use crate::saga_interface::SagaContext; use crate::sagas; -use crate::updates::ArtifactsDocument; use anyhow::Context; use async_trait::async_trait; use futures::future::ready; @@ -2262,72 +2261,26 @@ impl Nexus { self.db_datastore.session_hard_delete(token).await } - fn updates_load_artifacts( - &self, - ) -> Result< - Vec, - Box, - > { - // TODO(iliana): make async/.await. awslabs/tough#213 - use std::io::Read; - - let rack = self.as_rack(); - let repository = tough::RepositoryLoader::new( - self.tuf_trusted_root.as_slice(), - rack.tuf_metadata_base_url.parse()?, - rack.tuf_targets_base_url.parse()?, - ) - .load()?; - - let mut artifact_document = Vec::new(); - match repository.read_target(&"artifacts.json".parse()?)? { - Some(mut target) => target.read_to_end(&mut artifact_document)?, - None => return Err("artifacts.json missing".into()), - }; - let artifacts: ArtifactsDocument = - serde_json::from_slice(&artifact_document)?; - - let earliest_expiration = unimplemented!(); - - let mut v = Vec::new(); - for artifact in artifacts.artifacts { - if let Some(target) = repository - .targets() - .signed - .targets - .get(&artifact.target.parse()?) - { - v.push(db::model::UpdateAvailableArtifact { - name: artifact.name, - version: artifact.version, - kind: db::model::UpdateArtifactKind(artifact.kind), - targets_version: repository - .targets() - .signed - .version - .get() - .try_into()?, - metadata_expiration: earliest_expiration, - target_name: artifact.target, - target_sha256: hex::encode(&target.hashes.sha256), - target_length: target.length.try_into()?, - }); - } - } - Ok(v) - } - pub async fn updates_refresh_metadata(&self) -> Result<(), Error> { - for artifact in self.updates_load_artifacts().map_err(|e| { - Error::InternalError { internal_message: e.to_string() } - })? { + let rack = self.as_rack(); + let trust_root = self.tuf_trusted_root.clone(); + let artifacts = tokio::task::spawn_blocking(move || { + crate::updates::read_artifacts(&rack, &trust_root) + }) + .await + // first, the JoinError: + .map_err(|e| Error::InternalError { internal_message: e.to_string() })? + // next, the boxed dyn Error: + .map_err(|e| Error::InternalError { + internal_message: e.to_string(), + })?; + for artifact in artifacts { self.db_datastore .update_available_artifact_upsert(artifact) .await?; } - // delete all rows remaining with a targets_version < repository.targets().version() - unimplemented!(); + // TODO: delete all rows remaining with a targets_version < repository.targets().version() Ok(()) } diff --git a/nexus/src/updates.rs b/nexus/src/updates.rs index d8463797245..0b45240692e 100644 --- a/nexus/src/updates.rs +++ b/nexus/src/updates.rs @@ -1,5 +1,7 @@ +use crate::db; use parse_display::Display; use serde::Deserialize; +use std::convert::TryInto; // Schema for the `artifacts.json` target in the TUF update repository. #[derive(Clone, Debug, Deserialize)] @@ -21,3 +23,61 @@ pub struct UpdateArtifact { pub enum UpdateArtifactKind { Zone, } + +// TODO(iliana): make async/.await. awslabs/tough#213 +pub fn read_artifacts( + rack: &db::model::Rack, + tuf_trusted_root: &[u8], +) -> Result< + Vec, + Box, +> { + use std::io::Read; + + let repository = tough::RepositoryLoader::new( + tuf_trusted_root, + rack.tuf_metadata_base_url.parse()?, + rack.tuf_targets_base_url.parse()?, + ) + .load()?; + + let mut artifact_document = Vec::new(); + match repository.read_target(&"artifacts.json".parse()?)? { + Some(mut target) => target.read_to_end(&mut artifact_document)?, + None => return Err("artifacts.json missing".into()), + }; + let artifacts: ArtifactsDocument = + serde_json::from_slice(&artifact_document)?; + + let earliest_expiration = repository + .root() + .signed + .expires + .min(repository.snapshot().signed.expires) + .min(repository.targets().signed.expires) + .min(repository.timestamp().signed.expires); + + let mut v = Vec::new(); + for artifact in artifacts.artifacts { + if let Some(target) = + repository.targets().signed.targets.get(&artifact.target.parse()?) + { + v.push(db::model::UpdateAvailableArtifact { + name: artifact.name, + version: artifact.version, + kind: db::model::UpdateArtifactKind(artifact.kind), + targets_version: repository + .targets() + .signed + .version + .get() + .try_into()?, + metadata_expiration: earliest_expiration, + target_name: artifact.target, + target_sha256: hex::encode(&target.hashes.sha256), + target_length: target.length.try_into()?, + }); + } + } + Ok(v) +} From d6ea973d837920d9dbbae377a3e9d1327725beec Mon Sep 17 00:00:00 2001 From: iliana etaoin Date: Tue, 30 Nov 2021 23:02:53 +0000 Subject: [PATCH 03/14] allow an unconfigured updates system --- nexus/examples/config-file.toml | 10 +++++++--- nexus/examples/config.toml | 4 ++++ nexus/src/config.rs | 11 +++++++++-- nexus/src/nexus.rs | 19 ++++++++++++++----- smf/nexus/config.toml | 4 ++++ 5 files changed, 38 insertions(+), 10 deletions(-) diff --git a/nexus/examples/config-file.toml b/nexus/examples/config-file.toml index fb4cd61c048..a12d09b6e1a 100644 --- a/nexus/examples/config-file.toml +++ b/nexus/examples/config-file.toml @@ -40,6 +40,10 @@ level = "info" #mode = "stderr-terminal" # Example output to a file, appending if it already exists. -mode = "file" -path = "logs/server.log" -if_exists = "append" +#mode = "file" +#path = "logs/server.log" +#if_exists = "append" + +[updates] +# If not present, accessing the TUF updates repository will fail +#tuf_trusted_root = "" diff --git a/nexus/examples/config.toml b/nexus/examples/config.toml index c4091e73266..45576ec51e0 100644 --- a/nexus/examples/config.toml +++ b/nexus/examples/config.toml @@ -47,3 +47,7 @@ mode = "stderr-terminal" #mode = "file" #path = "logs/server.log" #if_exists = "append" + +[updates] +# If not present, accessing the TUF updates repository will fail +#tuf_trusted_root = "" diff --git a/nexus/src/config.rs b/nexus/src/config.rs index 2802fdf2adb..20dc2eac6f3 100644 --- a/nexus/src/config.rs +++ b/nexus/src/config.rs @@ -40,6 +40,13 @@ pub struct ConsoleConfig { pub session_absolute_timeout_minutes: u32, } +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct UpdatesConfig { + /** Trusted root.json role for the TUF updates repository. If `None`, accessing the TUF + * repository will fail. */ + pub tuf_trusted_root: Option, +} + /** * Configuration for a nexus server */ @@ -59,8 +66,8 @@ pub struct Config { pub database: db::Config, /** Authentication-related configuration */ pub authn: AuthnConfig, - - pub tuf_trusted_root: PathBuf, + /** Updates-related configuration */ + pub updates: UpdatesConfig, } #[derive(Debug)] diff --git a/nexus/src/nexus.rs b/nexus/src/nexus.rs index 5b15de015e1..77b298aa06e 100644 --- a/nexus/src/nexus.rs +++ b/nexus/src/nexus.rs @@ -128,7 +128,7 @@ pub struct Nexus { /** Task representing completion of recovered Sagas */ recovery_task: std::sync::Mutex>, - tuf_trusted_root: Vec, + tuf_trusted_root: Option>, } /* @@ -174,9 +174,10 @@ impl Nexus { db_datastore: Arc::clone(&db_datastore), sec_client: Arc::clone(&sec_client), recovery_task: std::sync::Mutex::new(None), - tuf_trusted_root: tokio::fs::read(&config.tuf_trusted_root) - .await - .unwrap(), + tuf_trusted_root: match &config.updates.tuf_trusted_root { + Some(root) => Some(tokio::fs::read(root).await.unwrap()), + None => None, + }, }; /* TODO-cleanup all the extra Arcs here seems wrong */ @@ -2263,7 +2264,14 @@ impl Nexus { pub async fn updates_refresh_metadata(&self) -> Result<(), Error> { let rack = self.as_rack(); - let trust_root = self.tuf_trusted_root.clone(); + let trust_root = self + .tuf_trusted_root + .as_ref() + .ok_or_else(|| Error::InvalidRequest { + message: "updates system not configured".into(), + })? + .clone(); + let artifacts = tokio::task::spawn_blocking(move || { crate::updates::read_artifacts(&rack, &trust_root) }) @@ -2274,6 +2282,7 @@ impl Nexus { .map_err(|e| Error::InternalError { internal_message: e.to_string(), })?; + for artifact in artifacts { self.db_datastore .update_available_artifact_upsert(artifact) diff --git a/smf/nexus/config.toml b/smf/nexus/config.toml index 402d4d5bcf2..b2781d68590 100644 --- a/smf/nexus/config.toml +++ b/smf/nexus/config.toml @@ -39,3 +39,7 @@ mode = "stderr-terminal" #mode = "file" #path = "logs/server.log" #if_exists = "append" + +[updates] +# If not present, accessing the TUF updates repository will fail +#tuf_trusted_root = "" From 6f074c2bd1b9fda2c02893df395c1292ebe2d380 Mon Sep 17 00:00:00 2001 From: iliana etaoin Date: Wed, 1 Dec 2021 19:15:16 +0000 Subject: [PATCH 04/14] woo license header --- nexus/src/updates.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nexus/src/updates.rs b/nexus/src/updates.rs index 0b45240692e..702be55d247 100644 --- a/nexus/src/updates.rs +++ b/nexus/src/updates.rs @@ -1,3 +1,7 @@ +// 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 crate::db; use parse_display::Display; use serde::Deserialize; From 90b444305d69a102a048eb5b24893cdc316cb019 Mon Sep 17 00:00:00 2001 From: iliana etaoin Date: Wed, 1 Dec 2021 19:19:48 +0000 Subject: [PATCH 05/14] fix hardcoded base URLs to use localhost, for now --- nexus/src/nexus.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/nexus/src/nexus.rs b/nexus/src/nexus.rs index 77b298aa06e..cb8aa6cabea 100644 --- a/nexus/src/nexus.rs +++ b/nexus/src/nexus.rs @@ -1981,10 +1981,8 @@ impl Nexus { fn as_rack(&self) -> db::model::Rack { db::model::Rack { identity: self.api_rack_identity.clone(), - // FIXME your username is embedded in a path ya dingus - tuf_metadata_base_url: "file:///home/iliana/tuf/metadata" - .to_string(), - tuf_targets_base_url: "file:///home/iliana/tuf/targets".to_string(), + tuf_metadata_base_url: "http://localhost:8000/metadata".to_string(), + tuf_targets_base_url: "http://localhost:8000/targets".to_string(), } } From 7475b47b6c9c84106ac501f68fbdb96b7c4645d0 Mon Sep 17 00:00:00 2001 From: iliana etaoin Date: Wed, 1 Dec 2021 19:28:06 +0000 Subject: [PATCH 06/14] add smf/nexus/root.json to .gitignore --- .gitignore | 1 + smf/nexus/config.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 8557fc69bc1..dce94bf4e21 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ tools/clickhouse* tools/cockroach* clickhouse/ cockroachdb/ +smf/nexus/root.json diff --git a/smf/nexus/config.toml b/smf/nexus/config.toml index b2781d68590..e849d6bb2d7 100644 --- a/smf/nexus/config.toml +++ b/smf/nexus/config.toml @@ -42,4 +42,4 @@ mode = "stderr-terminal" [updates] # If not present, accessing the TUF updates repository will fail -#tuf_trusted_root = "" +#tuf_trusted_root = "/opt/oxide/nexus/pkg/root.json" From 68402cca7475b68a5722964a1d68bed442a761ec Mon Sep 17 00:00:00 2001 From: iliana etaoin Date: Wed, 1 Dec 2021 19:39:46 +0000 Subject: [PATCH 07/14] rename columns to make more sense at first glance --- common/src/sql/dbinit.sql | 4 ++-- nexus/src/db/model.rs | 4 ++-- nexus/src/db/schema.rs | 4 ++-- nexus/src/updates.rs | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/common/src/sql/dbinit.sql b/common/src/sql/dbinit.sql index aae2eeae6fe..c1807701073 100644 --- a/common/src/sql/dbinit.sql +++ b/common/src/sql/dbinit.sql @@ -658,10 +658,10 @@ CREATE TABLE omicron.public.update_available_artifact ( kind omicron.public.update_artifact_kind NOT NULL, /* the version of the targets.json role this came from */ - targets_version INT NOT NULL, + targets_role_version INT NOT NULL, /* when the metadata this artifact was cached from expires */ - metadata_expiration TIMESTAMPTZ NOT NULL, + valid_until TIMESTAMPTZ NOT NULL, /* data about the target from the targets.json role */ target_name STRING(512) NOT NULL, diff --git a/nexus/src/db/model.rs b/nexus/src/db/model.rs index fb7412805b0..1196b39ca55 100644 --- a/nexus/src/db/model.rs +++ b/nexus/src/db/model.rs @@ -1800,8 +1800,8 @@ pub struct UpdateAvailableArtifact { pub kind: UpdateArtifactKind, /// `version` field of targets.json from the repository // FIXME this *should* be a NonZeroU64 - pub targets_version: i64, - pub metadata_expiration: DateTime, + pub targets_role_version: i64, + pub valid_until: DateTime, pub target_name: String, // FIXME should this be [u8; 32]? pub target_sha256: String, diff --git a/nexus/src/db/schema.rs b/nexus/src/db/schema.rs index b58769a23da..c51b73dd6a9 100644 --- a/nexus/src/db/schema.rs +++ b/nexus/src/db/schema.rs @@ -296,8 +296,8 @@ table! { name -> Text, version -> Int8, kind -> crate::db::model::UpdateArtifactKindEnum, - targets_version -> Int8, - metadata_expiration -> Timestamptz, + targets_role_version -> Int8, + valid_until -> Timestamptz, target_name -> Text, target_sha256 -> Text, target_length -> Int8, diff --git a/nexus/src/updates.rs b/nexus/src/updates.rs index 702be55d247..6faf3b7c597 100644 --- a/nexus/src/updates.rs +++ b/nexus/src/updates.rs @@ -53,7 +53,7 @@ pub fn read_artifacts( let artifacts: ArtifactsDocument = serde_json::from_slice(&artifact_document)?; - let earliest_expiration = repository + let valid_until = repository .root() .signed .expires @@ -70,13 +70,13 @@ pub fn read_artifacts( name: artifact.name, version: artifact.version, kind: db::model::UpdateArtifactKind(artifact.kind), - targets_version: repository + targets_role_version: repository .targets() .signed .version .get() .try_into()?, - metadata_expiration: earliest_expiration, + valid_until, target_name: artifact.target, target_sha256: hex::encode(&target.hashes.sha256), target_length: target.length.try_into()?, From 1ee0f5bebadce661edb8c6b25e9817c39eb1341e Mon Sep 17 00:00:00 2001 From: iliana etaoin Date: Wed, 1 Dec 2021 20:11:51 +0000 Subject: [PATCH 08/14] keep table in sync with artifacts.json --- nexus/src/db/datastore.rs | 20 ++++++++++++++++++++ nexus/src/nexus.rs | 13 ++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/nexus/src/db/datastore.rs b/nexus/src/db/datastore.rs index 4b47fb17e95..befb567b572 100644 --- a/nexus/src/db/datastore.rs +++ b/nexus/src/db/datastore.rs @@ -1960,6 +1960,26 @@ impl DataStore { ) }) } + + pub async fn update_available_artifact_hard_delete_outdated( + &self, + current_targets_role_version: i64, + ) -> DeleteResult { + // We use the `targets_role_version` column in the table to delete any old rows, keeping + // the table in sync with the current copy of artifacts.json. + use db::schema::update_available_artifact::dsl; + diesel::delete(dsl::update_available_artifact) + .filter(dsl::targets_role_version.lt(current_targets_role_version)) + .execute_async(self.pool()) + .await + .map(|_rows_deleted| ()) + .map_err(|e| { + Error::internal_error(&format!( + "error deleting outdated available artifacts: {:?}", + e + )) + }) + } } #[cfg(test)] diff --git a/nexus/src/nexus.rs b/nexus/src/nexus.rs index cb8aa6cabea..2b88754e8e4 100644 --- a/nexus/src/nexus.rs +++ b/nexus/src/nexus.rs @@ -2281,13 +2281,24 @@ impl Nexus { internal_message: e.to_string(), })?; + // FIXME: if we hit an error in any of these database calls, the available artifact table + // will be out of sync with the current artifacts.json. can we do a transaction or + // something? + + let mut current_version = None; for artifact in artifacts { + current_version = Some(artifact.targets_role_version); self.db_datastore .update_available_artifact_upsert(artifact) .await?; } - // TODO: delete all rows remaining with a targets_version < repository.targets().version() + // ensure table is in sync with current copy of artifacts.json + if let Some(current_version) = current_version { + self.db_datastore + .update_available_artifact_hard_delete_outdated(current_version) + .await?; + } Ok(()) } From 873e439fe9871215d6d61bd76acfb7121332ad71 Mon Sep 17 00:00:00 2001 From: iliana etaoin Date: Wed, 1 Dec 2021 20:37:05 +0000 Subject: [PATCH 09/14] make the tests happy --- nexus/src/config.rs | 6 ++++-- nexus/tests/common/mod.rs | 2 +- openapi/nexus.json | 11 +++++++++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/nexus/src/config.rs b/nexus/src/config.rs index 20dc2eac6f3..1088e658a0a 100644 --- a/nexus/src/config.rs +++ b/nexus/src/config.rs @@ -40,7 +40,7 @@ pub struct ConsoleConfig { pub session_absolute_timeout_minutes: u32, } -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] pub struct UpdatesConfig { /** Trusted root.json role for the TUF updates repository. If `None`, accessing the TUF * repository will fail. */ @@ -67,6 +67,7 @@ pub struct Config { /** Authentication-related configuration */ pub authn: AuthnConfig, /** Updates-related configuration */ + #[serde(default)] pub updates: UpdatesConfig, } @@ -173,7 +174,7 @@ impl Config { mod test { use super::{ AuthnConfig, Config, ConsoleConfig, LoadError, LoadErrorKind, - SchemeName, + SchemeName, UpdatesConfig, }; use crate::db; use dropshot::ConfigDropshot; @@ -339,6 +340,7 @@ mod test { .parse() .unwrap() }, + updates: UpdatesConfig { tuf_trusted_root: None }, } ); diff --git a/nexus/tests/common/mod.rs b/nexus/tests/common/mod.rs index 9dfc9c0a117..2b5aa3a9613 100644 --- a/nexus/tests/common/mod.rs +++ b/nexus/tests/common/mod.rs @@ -101,7 +101,7 @@ pub async fn test_setup_with_config( let clickhouse = dev::clickhouse::ClickHouseInstance::new(0).await.unwrap(); config.database.url = database.pg_config().clone(); - let server = omicron_nexus::Server::start(&config, &rack_id, &logctx.log) + let server = omicron_nexus::Server::start(&config, rack_id, &logctx.log) .await .unwrap(); let testctx_external = ClientTestContext::new( diff --git a/openapi/nexus.json b/openapi/nexus.json index f139df5cc0c..38af7f9fb30 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -2703,6 +2703,17 @@ } } } + }, + "/updates/refresh": { + "post": { + "description": "Refresh update metadata", + "operationId": "updates_refresh", + "responses": { + "200": { + "description": "successful operation" + } + } + } } }, "components": { From b2c0851b2a574823a178887b02a9343a5e6f64b0 Mon Sep 17 00:00:00 2001 From: iliana etaoin Date: Thu, 2 Dec 2021 21:28:04 +0000 Subject: [PATCH 10/14] add /updates/refresh to oxapi_demo --- tools/oxapi_demo | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tools/oxapi_demo b/tools/oxapi_demo index 4afbb0f66ff..4bc14fd1395 100755 --- a/tools/oxapi_demo +++ b/tools/oxapi_demo @@ -72,6 +72,10 @@ HARDWARE sleds_list sled_get SLED_ID + +UPDATES + + updates_refresh EOF )" @@ -363,4 +367,9 @@ function cmd_sled_get do_curl "/hardware/sleds/$1" } +function cmd_updates_refresh +{ + do_curl /updates/refresh -X POST +} + main "$@" From c1b03f43283fcbab95c888e7951c74778ba84ad3 Mon Sep 17 00:00:00 2001 From: iliana etaoin Date: Thu, 2 Dec 2021 21:34:18 +0000 Subject: [PATCH 11/14] fix examples/config-file.toml --- nexus/examples/config-file.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nexus/examples/config-file.toml b/nexus/examples/config-file.toml index a12d09b6e1a..b0a285ec370 100644 --- a/nexus/examples/config-file.toml +++ b/nexus/examples/config-file.toml @@ -40,9 +40,9 @@ level = "info" #mode = "stderr-terminal" # Example output to a file, appending if it already exists. -#mode = "file" -#path = "logs/server.log" -#if_exists = "append" +mode = "file" +path = "logs/server.log" +if_exists = "append" [updates] # If not present, accessing the TUF updates repository will fail From 8295a3a66d276e8f80743687cadb8ca5e461952d Mon Sep 17 00:00:00 2001 From: iliana etaoin Date: Thu, 2 Dec 2021 21:40:41 +0000 Subject: [PATCH 12/14] test [updates] in config works --- nexus/src/config.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/nexus/src/config.rs b/nexus/src/config.rs index 1088e658a0a..859d4d11add 100644 --- a/nexus/src/config.rs +++ b/nexus/src/config.rs @@ -303,6 +303,8 @@ mod test { level = "debug" path = "/nonexistent/path" if_exists = "fail" + [updates] + tuf_trusted_root = "/path/to/root.json" "##, ) .unwrap(); @@ -340,7 +342,9 @@ mod test { .parse() .unwrap() }, - updates: UpdatesConfig { tuf_trusted_root: None }, + updates: UpdatesConfig { + tuf_trusted_root: Some(PathBuf::from("/path/to/root.json")) + }, } ); From f95628f9445fb907daa03d6e68571ef3e811ac29 Mon Sep 17 00:00:00 2001 From: iliana etaoin Date: Thu, 2 Dec 2021 21:40:51 +0000 Subject: [PATCH 13/14] remove dead code --- nexus/src/external_api/http_entrypoints.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 7974d450883..3c00ce37f31 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -1883,7 +1883,6 @@ async fn hardware_sleds_get_sled( }] async fn updates_refresh( rqctx: Arc>>, - // _query_params: Query, ) -> Result, HttpError> { let apictx = rqctx.context(); let nexus = &apictx.nexus; From e8727457d2132eb1a22b5f50352eb961ee2c147e Mon Sep 17 00:00:00 2001 From: iliana etaoin Date: Thu, 2 Dec 2021 21:41:07 +0000 Subject: [PATCH 14/14] .unwrap() on JoinError from spawn_blocking ... since We only get a JoinError if the task we're waiting on panics. --- nexus/src/nexus.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/nexus/src/nexus.rs b/nexus/src/nexus.rs index 2b88754e8e4..5e06da74df6 100644 --- a/nexus/src/nexus.rs +++ b/nexus/src/nexus.rs @@ -2274,11 +2274,9 @@ impl Nexus { crate::updates::read_artifacts(&rack, &trust_root) }) .await - // first, the JoinError: - .map_err(|e| Error::InternalError { internal_message: e.to_string() })? - // next, the boxed dyn Error: + .unwrap() .map_err(|e| Error::InternalError { - internal_message: e.to_string(), + internal_message: format!("error trying to refresh updates: {}", e), })?; // FIXME: if we hit an error in any of these database calls, the available artifact table