Skip to content

Commit

Permalink
Add FloatingIp view, resource handling + authz
Browse files Browse the repository at this point in the history
  • Loading branch information
FelixMcFelix committed Nov 24, 2023
1 parent 73d9f0d commit c826bb3
Show file tree
Hide file tree
Showing 13 changed files with 285 additions and 99 deletions.
2 changes: 1 addition & 1 deletion common/src/api/external/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -751,7 +751,7 @@ pub enum ResourceType {
Zpool,
Vmm,
Ipv4NatEntry,
// ExternalIp,
FloatingIp,
}

// IDENTITY METADATA
Expand Down
50 changes: 49 additions & 1 deletion nexus/db-model/src/external_ip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
//! services.
use crate::impl_enum_type;
use crate::schema::external_ip;
use crate::schema::{external_ip, floating_ip};
use crate::Name;
use crate::SqlU16;
use chrono::DateTime;
use chrono::Utc;
use db_macros::Resource;
use diesel::Queryable;
use diesel::Selectable;
use ipnetwork::IpNetwork;
Expand All @@ -19,6 +20,7 @@ use nexus_types::external_api::views;
use omicron_common::address::NUM_SOURCE_NAT_PORTS;
use omicron_common::api::external::Error;
use omicron_common::api::external::IdentityMetadata;
use serde::{Deserialize, Serialize};
use std::convert::TryFrom;
use std::net::IpAddr;
use uuid::Uuid;
Expand Down Expand Up @@ -74,6 +76,28 @@ pub struct ExternalIp {
pub project_id: Option<Uuid>,
}

/// A view type constructed from `ExternalIp` used to represent Floating IP
/// objects in user-facing APIs.
///
/// This View type fills a similar niche to `ProjectImage` etc.: we need to
/// represent identity as non-nullable (ditto for parent project) so as to
/// play nicely with authz and resource APIs.
#[derive(
Queryable, Selectable, Clone, Debug, Resource, Serialize, Deserialize,
)]
#[diesel(table_name = floating_ip)]
pub struct FloatingIp {
#[diesel(embed)]
pub identity: FloatingIpIdentity,

pub ip_pool_id: Uuid,
pub ip_pool_range_id: Uuid,
pub is_service: bool,
pub parent_id: Option<Uuid>,
pub ip: IpNetwork,
pub project_id: Uuid,
}

impl From<ExternalIp> for sled_agent_client::types::SourceNatConfig {
fn from(eip: ExternalIp) -> Self {
Self {
Expand Down Expand Up @@ -352,6 +376,11 @@ impl TryFrom<ExternalIp> for views::FloatingIp {
type Error = Error;

fn try_from(ip: ExternalIp) -> Result<Self, Self::Error> {
if ip.kind != IpKind::Floating {
return Err(Error::internal_error(
"attempted to convert non-floating external IP to floating",
));
}
if ip.is_service {
return Err(Error::internal_error(
"Service IPs should not be exposed in the API",
Expand Down Expand Up @@ -389,3 +418,22 @@ impl TryFrom<ExternalIp> for views::FloatingIp {
})
}
}

impl From<FloatingIp> for views::FloatingIp {
fn from(ip: FloatingIp) -> Self {
let identity = IdentityMetadata {
id: ip.identity.id,
name: ip.identity.name.into(),
description: ip.identity.description,
time_created: ip.identity.time_created,
time_modified: ip.identity.time_modified,
};

views::FloatingIp {
ip: ip.ip.ip(),
identity,
project_id: ip.project_id,
instance_id: ip.parent_id,
}
}
}
18 changes: 18 additions & 0 deletions nexus/db-model/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,24 @@ table! {
}
}

table! {
floating_ip (id) {
id -> Uuid,
name -> Text,
description -> Text,
time_created -> Timestamptz,
time_modified -> Timestamptz,
time_deleted -> Nullable<Timestamptz>,

ip_pool_id -> Uuid,
ip_pool_range_id -> Uuid,
is_service -> Bool,
parent_id -> Nullable<Uuid>,
ip -> Inet,
project_id -> Uuid,
}
}

table! {
silo (id) {
id -> Uuid,
Expand Down
8 changes: 8 additions & 0 deletions nexus/db-queries/src/authz/api_resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -791,6 +791,14 @@ authz_resource! {
polar_snippet = InProject,
}

authz_resource! {
name = "FloatingIp",
parent = "Project",
primary_key = Uuid,
roles_allowed = false,
polar_snippet = InProject,
}

// Customer network integration resources nested below "Fleet"

authz_resource! {
Expand Down
1 change: 1 addition & 0 deletions nexus/db-queries/src/authz/oso_generic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ pub fn make_omicron_oso(log: &slog::Logger) -> Result<OsoInit, anyhow::Error> {
VpcRouter::init(),
RouterRoute::init(),
VpcSubnet::init(),
FloatingIp::init(),
// Silo-level resources
Image::init(),
SiloImage::init(),
Expand Down
103 changes: 91 additions & 12 deletions nexus/db-queries/src/db/datastore/external_ip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ use crate::db::error::public_error_from_diesel;
use crate::db::error::ErrorHandler;
use crate::db::lookup::LookupPath;
use crate::db::model::ExternalIp;
use crate::db::model::FloatingIp;
use crate::db::model::IncompleteExternalIp;
use crate::db::model::IpKind;
use crate::db::model::Name;
use crate::db::pagination::paginated;
use crate::db::pool::DbConnection;
use crate::db::queries::external_ip::NextExternalIp;
use crate::db::update_and_check::UpdateAndCheck;
Expand All @@ -24,11 +26,15 @@ use async_bb8_diesel::AsyncRunQueryDsl;
use chrono::Utc;
use diesel::prelude::*;
use nexus_types::identity::Resource;
use omicron_common::api::external::http_pagination::PaginatedBy;
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::NameOrId;
use omicron_common::api::external::UpdateResult;
use ref_cast::RefCast;
use std::net::IpAddr;
use uuid::Uuid;

Expand Down Expand Up @@ -344,8 +350,6 @@ impl DataStore {
/// This method returns the number of records deleted, rather than the usual
/// `DeleteResult`. That's mostly useful for tests, but could be important
/// if callers have some invariants they'd like to check.
// TODO-correctness: This can't be used for Floating IPs, we'll need a
// _detatch_ method for that.
pub async fn deallocate_external_ip_by_instance_id(
&self,
opctx: &OpContext,
Expand All @@ -364,6 +368,27 @@ impl DataStore {
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))
}

/// Detach an individual Floating IP address from its parent instance.
///
/// As in `deallocate_external_ip_by_instance_id`, This method returns the
/// number of records deleted, rather than the usual `DeleteResult`.
pub async fn detach_floating_ips_by_instance_id(
&self,
opctx: &OpContext,
instance_id: Uuid,
) -> Result<usize, Error> {
use db::schema::external_ip::dsl;
diesel::update(dsl::external_ip)
.filter(dsl::time_deleted.is_null())
.filter(dsl::is_service.eq(false))
.filter(dsl::parent_id.eq(instance_id))
.filter(dsl::kind.eq(IpKind::Floating))
.set(dsl::parent_id.eq(Option::<Uuid>::None))
.execute_async(&*self.pool_connection_authorized(opctx).await?)
.await
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))
}

/// Fetch all external IP addresses of any kind for the provided instance
pub async fn instance_lookup_external_ips(
&self,
Expand All @@ -381,20 +406,74 @@ impl DataStore {
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))
}

/// Fetch all floating IP addresses for the provided project.
pub async fn lookup_floating_ips(
/// Fetch all Floating IP addresses for the provided project.
pub async fn floating_ips_list(
&self,
opctx: &OpContext,
project_id: Uuid,
) -> LookupResult<Vec<ExternalIp>> {
authz_project: &authz::Project,
pagparams: &PaginatedBy<'_>,
) -> ListResultVec<FloatingIp> {
opctx.authorize(authz::Action::ListChildren, authz_project).await?;

use db::schema::floating_ip::dsl;

match pagparams {
PaginatedBy::Id(pagparams) => {
paginated(dsl::floating_ip, dsl::id, &pagparams)
}
PaginatedBy::Name(pagparams) => paginated(
dsl::floating_ip,
dsl::name,
&pagparams.map_name(|n| Name::ref_cast(n)),
),
}
.filter(dsl::project_id.eq(authz_project.id()))
.filter(dsl::time_deleted.is_null())
.select(FloatingIp::as_select())
.get_results_async(&*self.pool_connection_authorized(opctx).await?)
.await
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))
}

/// Delete a Floating IP, verifying first that it is not in use.
pub async fn floating_ip_delete(
&self,
opctx: &OpContext,
authz_fip: &authz::FloatingIp,
db_fip: &FloatingIp,
) -> DeleteResult {
use db::schema::external_ip::dsl;
dsl::external_ip
.filter(dsl::project_id.eq(project_id))

// Verify this FIP is not attached to any instances/services.
if db_fip.parent_id.is_some() {
return Err(Error::invalid_request(
"Floating IP cannot be deleted while attached to an instance",
));
}

opctx.authorize(authz::Action::Delete, authz_fip).await?;

let now = Utc::now();
let updated_rows = diesel::update(dsl::external_ip)
.filter(dsl::id.eq(db_fip.id()))
.filter(dsl::time_deleted.is_null())
.filter(dsl::kind.eq(IpKind::Floating))
.select(ExternalIp::as_select())
.get_results_async(&*self.pool_connection_authorized(opctx).await?)
.filter(dsl::parent_id.is_null())
.set(dsl::time_deleted.eq(now))
.execute_async(&*self.pool_connection_authorized(opctx).await?)
.await
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))
.map_err(|e| {
public_error_from_diesel(
e,
ErrorHandler::NotFoundByResource(authz_fip),
)
})?;

if updated_rows == 0 {
return Err(Error::InvalidRequest {
message: "deletion failed due to concurrent modification"
.to_string(),
});
}
Ok(())
}
}
10 changes: 0 additions & 10 deletions nexus/db-queries/src/db/datastore/ip_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,16 +110,6 @@ impl DataStore {
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))
}

/// Lookup an IP pool within the current silo which contains a target IP
/// address.
pub async fn ip_pools_fetch_for_ip(
&self,
opctx: &OpContext,
ip_addr: std::net::IpAddr,
) -> LookupResult<IpPool> {
todo!()
}

/// Looks up an IP pool intended for internal services.
///
/// This method may require an index by Availability Zone in the future.
Expand Down
24 changes: 14 additions & 10 deletions nexus/db-queries/src/db/lookup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,11 @@ impl<'a> LookupPath<'a> {
RouterRoute::PrimaryKey(Root { lookup_root: self }, id)
}

/// Select a resource of type FloatingIp, identified by its id
pub fn floating_ip_id(self, id: Uuid) -> FloatingIp<'a> {
FloatingIp::PrimaryKey(Root { lookup_root: self }, id)
}

// Fleet-level resources

/// Select a resource of type ConsoleSession, identified by its `token`
Expand Down Expand Up @@ -632,8 +637,7 @@ lookup_resource! {
lookup_resource! {
name = "Project",
ancestors = [ "Silo" ],
// children = [ "Disk", "Instance", "Vpc", "Snapshot", "ProjectImage", "ExternalIp" ],
children = [ "Disk", "Instance", "Vpc", "Snapshot", "ProjectImage" ],
children = [ "Disk", "Instance", "Vpc", "Snapshot", "ProjectImage", "FloatingIp" ],
lookup_by_name = true,
soft_deletes = true,
primary_key_columns = [ { column_name = "id", rust_type = Uuid } ]
Expand Down Expand Up @@ -729,14 +733,14 @@ lookup_resource! {
primary_key_columns = [ { column_name = "id", rust_type = Uuid } ]
}

// lookup_resource! {
// name = "ExternalIp",
// ancestors = [ "Silo", "Project" ],
// children = [],
// lookup_by_name = true,
// soft_deletes = true,
// primary_key_columns = [ { column_name = "id", rust_type = Uuid } ]
// }
lookup_resource! {
name = "FloatingIp",
ancestors = [ "Silo", "Project" ],
children = [],
lookup_by_name = true,
soft_deletes = true,
primary_key_columns = [ { column_name = "id", rust_type = Uuid } ]
}

// Miscellaneous resources nested directly below "Fleet"

Expand Down
Loading

0 comments on commit c826bb3

Please sign in to comment.