diff --git a/nexus/db-model/src/ipv4_nat_entry.rs b/nexus/db-model/src/ipv4_nat_entry.rs new file mode 100644 index 00000000000..96dcb4cb983 --- /dev/null +++ b/nexus/db-model/src/ipv4_nat_entry.rs @@ -0,0 +1,46 @@ +use super::MacAddr; +use crate::{schema::ipv4_nat_entry, SqlU16, SqlU32, Vni}; +use chrono::{DateTime, Utc}; +use uuid::Uuid; + +/// Database representation of an Ipv4 NAT Entry. +#[derive(Insertable, Debug, Clone)] +#[diesel(table_name = ipv4_nat_entry)] +pub struct NewIpv4NatEntry { + pub id: Uuid, + pub external_address: ipnetwork::IpNetwork, + pub first_port: SqlU16, + pub last_port: SqlU16, + pub sled_address: ipnetwork::IpNetwork, + pub vni: Vni, + pub mac: MacAddr, +} + +#[derive(Queryable, Debug, Clone, Selectable)] +#[diesel(table_name = ipv4_nat_entry)] +pub struct Ipv4NatEntry { + pub id: Uuid, + pub external_address: ipnetwork::IpNetwork, + pub first_port: SqlU16, + pub last_port: SqlU16, + pub sled_address: ipnetwork::IpNetwork, + pub vni: Vni, + pub mac: MacAddr, + pub gen: SqlU32, + pub time_created: DateTime, + pub time_deleted: Option>, +} + +impl Ipv4NatEntry { + pub fn first_port(&self) -> u16 { + self.first_port.into() + } + + pub fn last_port(&self) -> u16 { + self.first_port.into() + } + + pub fn gen(&self) -> u32 { + self.gen.into() + } +} diff --git a/nexus/db-queries/src/db/datastore/ipv4_nat_entry.rs b/nexus/db-queries/src/db/datastore/ipv4_nat_entry.rs new file mode 100644 index 00000000000..7936f4c2468 --- /dev/null +++ b/nexus/db-queries/src/db/datastore/ipv4_nat_entry.rs @@ -0,0 +1,292 @@ +use super::DataStore; +use crate::context::OpContext; +use crate::db; +use crate::db::error::public_error_from_diesel_pool; +use crate::db::error::ErrorHandler; +use crate::db::model::{Ipv4NatEntry, NewIpv4NatEntry}; +use async_bb8_diesel::AsyncRunQueryDsl; +use chrono::{DateTime, Utc}; +use diesel::prelude::*; +use nexus_db_model::SqlU32; +use omicron_common::api::external::CreateResult; +use omicron_common::api::external::DeleteResult; +use omicron_common::api::external::Error; +use omicron_common::api::external::ListResultVec; +use omicron_common::api::external::LookupResult; +use omicron_common::api::external::ResourceType; +use uuid::Uuid; + +impl DataStore { + pub async fn ipv4_nat_create( + &self, + opctx: &OpContext, + nat_entry: NewIpv4NatEntry, + ) -> CreateResult { + use db::schema::ipv4_nat_entry::dsl; + + // TODO: What authorization do we need? + + diesel::insert_into(dsl::ipv4_nat_entry) + .values(nat_entry.clone()) + .on_conflict_do_nothing() + .returning(Ipv4NatEntry::as_returning()) + .get_result_async(self.pool_authorized(opctx).await?) + .await + .map_err(|e| { + public_error_from_diesel_pool( + e, + ErrorHandler::Conflict( + ResourceType::Ipv4NatEntry, + &nat_entry.id.to_string(), + ), + ) + }) + } + + pub async fn ipv4_nat_delete( + &self, + opctx: &OpContext, + nat_entry: &Ipv4NatEntry, + ) -> DeleteResult { + use db::schema::ipv4_nat_entry::dsl; + + // TODO: What authorization do we need? + + let now = Utc::now(); + let updated_rows = diesel::update(dsl::ipv4_nat_entry) + .filter(dsl::time_deleted.is_null()) + .filter(dsl::id.eq(nat_entry.id)) + .filter(dsl::gen.eq(nat_entry.gen)) + .set(dsl::time_deleted.eq(now)) + .execute_async(self.pool_authorized(opctx).await?) + .await + .map_err(|e| { + public_error_from_diesel_pool(e, ErrorHandler::Server) + })?; + + if updated_rows == 0 { + return Err(Error::InvalidRequest { + message: "no matching records".to_string(), + }); + } + Ok(()) + } + + pub async fn ipv4_nat_cancel_delete( + &self, + opctx: &OpContext, + nat_entry: &Ipv4NatEntry, + ) -> DeleteResult { + use db::schema::ipv4_nat_entry::dsl; + + // TODO: What authorization do we need? + + let updated_rows = diesel::update(dsl::ipv4_nat_entry) + .filter(dsl::time_deleted.is_not_null()) + .filter(dsl::id.eq(nat_entry.id)) + .filter(dsl::gen.eq(nat_entry.gen)) + .set(dsl::time_deleted.eq(Option::>::None)) + .execute_async(self.pool_authorized(opctx).await?) + .await + .map_err(|e| { + public_error_from_diesel_pool(e, ErrorHandler::Server) + })?; + + if updated_rows == 0 { + return Err(Error::InvalidRequest { + message: "no matching records".to_string(), + }); + } + Ok(()) + } + + pub async fn ipv4_nat_find_by_id( + &self, + opctx: &OpContext, + id: Uuid, + ) -> LookupResult { + use db::schema::ipv4_nat_entry::dsl; + + // TODO: What authorization do we need? + + let result = dsl::ipv4_nat_entry + .filter(dsl::id.eq(id)) + .select(Ipv4NatEntry::as_select()) + .limit(1) + .load_async(self.pool_authorized(opctx).await?) + .await + .map_err(|e| { + public_error_from_diesel_pool(e, ErrorHandler::Server) + })?; + + if result.is_empty() { + return Err(Error::InvalidRequest { + message: "no matching records".to_string(), + }); + } + + Ok(result.first().unwrap().clone()) + } + + pub async fn ipv4_nat_list_since_gen( + &self, + opctx: &OpContext, + gen: u32, + ) -> ListResultVec { + use db::schema::ipv4_nat_entry::dsl; + let gen: SqlU32 = gen.into(); + + // TODO: What authorization do we need? + + let list = dsl::ipv4_nat_entry + .filter(dsl::gen.gt(gen)) + .order_by(dsl::gen) + .select(Ipv4NatEntry::as_select()) + .load_async(self.pool_authorized(opctx).await?) + .await + .map_err(|e| { + public_error_from_diesel_pool(e, ErrorHandler::Server) + })?; + + Ok(list) + } + + pub async fn ipv4_nat_current_gen( + &self, + opctx: &OpContext, + ) -> LookupResult { + use db::schema::ipv4_nat_entry::dsl; + + // TODO: What authorization do we need? + + let latest = dsl::ipv4_nat_entry + .order_by(dsl::gen.desc()) + .select(Ipv4NatEntry::as_select()) + .limit(1) + .load_async(self.pool_authorized(opctx).await?) + .await + .map_err(|e| { + public_error_from_diesel_pool(e, ErrorHandler::Server) + })?; + + if latest.is_empty() { + return Ok(0); + } + + Ok(*latest.first().unwrap().gen) + } + + pub async fn ipv4_nat_cleanup( + &self, + opctx: &OpContext, + before_gen: u32, + before_timestamp: DateTime, + ) -> DeleteResult { + // TODO: What authorization do we need? + todo!() + } +} + +#[cfg(test)] +mod test { + use std::str::FromStr; + + use crate::db::datastore::datastore_test; + use nexus_db_model::{MacAddr, NewIpv4NatEntry, Vni}; + use nexus_test_utils::db::test_setup_database; + use omicron_common::api::external; + use omicron_test_utils::dev; + use uuid::Uuid; + + // Test our ability to track additions and deletions since a given generation number + #[tokio::test] + async fn nat_generation_tracking() { + let logctx = dev::test_setup_log("test_nat_generation_tracking"); + let mut db = test_setup_database(&logctx.log).await; + let (opctx, datastore) = datastore_test(&logctx, &db).await; + + // We should not have any NAT entries at this moment + let initial_state = + datastore.ipv4_nat_list_since_gen(&opctx, 0).await.unwrap(); + + assert!(initial_state.is_empty()); + assert_eq!(datastore.ipv4_nat_current_gen(&opctx).await.unwrap(), 0); + + // Each change (creation / deletion) to the NAT table should increment the + // generation number of the row in the NAT table + let external_address = + ipnetwork::IpNetwork::try_from("10.0.0.100").unwrap(); + + let sled_address = + ipnetwork::IpNetwork::try_from("fd00:1122:3344:104::1").unwrap(); + + // Add a nat entry. + let nat1 = NewIpv4NatEntry { + id: Uuid::new_v4(), + external_address, + first_port: 0.into(), + last_port: 999.into(), + sled_address, + vni: Vni(external::Vni::random()), + mac: MacAddr( + external::MacAddr::from_str("A8:40:25:F5:EB:2A").unwrap(), + ), + }; + + let first_entry = + datastore.ipv4_nat_create(&opctx, nat1).await.unwrap(); + + let nat_entries = + datastore.ipv4_nat_list_since_gen(&opctx, 0).await.unwrap(); + + // The NAT table has undergone one change. One entry has been added, + // none deleted, so we should be at generation 1. + assert_eq!(nat_entries.len(), 1); + assert_eq!(nat_entries.last().unwrap().gen(), 1); + assert_eq!(datastore.ipv4_nat_current_gen(&opctx).await.unwrap(), 1); + + // Add another nat entry. + let nat2 = NewIpv4NatEntry { + id: Uuid::new_v4(), + external_address, + first_port: 1000.into(), + last_port: 1999.into(), + sled_address, + vni: Vni(external::Vni::random()), + mac: MacAddr( + external::MacAddr::from_str("A8:40:25:F5:EB:2B").unwrap(), + ), + }; + + datastore.ipv4_nat_create(&opctx, nat2).await.unwrap(); + + let nat_entries = + datastore.ipv4_nat_list_since_gen(&opctx, 0).await.unwrap(); + + // The NAT table has undergone two changes. Two entries have been + // added, none deleted, so we should be at generation 2. + assert_eq!(nat_entries.len(), 2); + assert_eq!(nat_entries.last().unwrap().gen(), 2); + assert_eq!(datastore.ipv4_nat_current_gen(&opctx).await.unwrap(), 2); + + // Delete the first nat entry. It should show up as a later generation number. + datastore.ipv4_nat_delete(&opctx, &first_entry).await.unwrap(); + let nat_entries = + datastore.ipv4_nat_list_since_gen(&opctx, 0).await.unwrap(); + + // The NAT table has undergone three changes. Two entries have been + // added, one deleted, so we should be at generation 3. Since the + // first entry was marked for deletion (and it was the third change), + // the first entry's generation number should now be 3. + let nat_entry = nat_entries.last().unwrap(); + assert_eq!(nat_entries.len(), 2); + assert_eq!(nat_entry.gen(), 3); + assert_eq!(nat_entry.id, first_entry.id); + assert_eq!(datastore.ipv4_nat_current_gen(&opctx).await.unwrap(), 3); + + // TODO test cleanup logic + // TODO test delete cancellation logic + db.cleanup().await.unwrap(); + logctx.cleanup_successful(); + } +}