From bd0c7ba0a8b65c0aa80120f12603f038d601043d Mon Sep 17 00:00:00 2001 From: brian Date: Tue, 14 Jul 2026 12:45:25 -0400 Subject: [PATCH] feat(api): Added support for automatic prefix selection by VPC ID --- crates/admin-cli/src/instance/show/cmd.rs | 42 + crates/agent/src/tests/full.rs | 1 + crates/api-core/src/handlers/instance.rs | 30 +- crates/api-core/src/instance/mod.rs | 1060 +- .../api-core/src/network_segment/allocate.rs | 208 +- crates/api-core/src/tests/instance.rs | 946 +- .../src/tests/instance_config_update.rs | 756 +- crates/api-core/src/tests/vpc_prefix.rs | 11 +- crates/api-db/src/instance_address.rs | 13 +- crates/api-db/src/instance_network_config.rs | 1 + crates/api-db/src/vpc_prefix.rs | 88 +- crates/api-integration-tests/tests/lib.rs | 4 + .../api-model/src/instance/config/network.rs | 600 +- .../api-model/src/instance/status/network.rs | 72 +- crates/api-test-helper/src/instance.rs | 3 +- crates/machine-controller/src/handler.rs | 23 +- crates/rpc/build.rs | 8 + crates/rpc/proto/forge.proto | 57 +- .../rpc/src/model/instance/config/network.rs | 408 +- .../rpc/src/model/instance/status/network.rs | 44 + rest-api/docs/index.html | 382 +- rest-api/proto/core/gen/v1/nico_nico.pb.go | 11547 ++++++++-------- .../proto/core/gen/v1/nico_nico_grpc.pb.go | 16 +- rest-api/proto/core/src/v1/nico_nico.proto | 57 +- 24 files changed, 10147 insertions(+), 6230 deletions(-) diff --git a/crates/admin-cli/src/instance/show/cmd.rs b/crates/admin-cli/src/instance/show/cmd.rs index 18a4b54a9e..a77258b20c 100644 --- a/crates/admin-cli/src/instance/show/cmd.rs +++ b/crates/admin-cli/src/instance/show/cmd.rs @@ -263,9 +263,51 @@ async fn convert_instance_to_nice_format( Some( forgerpc::instance_interface_config::NetworkDetails::VpcPrefixId(x), ) => x.to_string().into(), + Some(forgerpc::instance_interface_config::NetworkDetails::Vpc(_)) => { + "Automatic VPC selection".into() + } None => "NA".into(), }, ), + ( + "REQUESTED VPC ID", + match if_config.and_then(|c| c.network_details.as_ref()) { + Some(forgerpc::instance_interface_config::NetworkDetails::Vpc( + selection, + )) => selection + .vpc_id + .map(|vpc_id| vpc_id.to_string().into()) + .unwrap_or_else(|| "NA".into()), + _ => "NA".into(), + }, + ), + ( + "REQUESTED IP FAMILY", + match if_config.and_then(|c| c.network_details.as_ref()) { + Some(forgerpc::instance_interface_config::NetworkDetails::Vpc( + selection, + )) => selection.family_mode().as_str_name().into(), + _ => "NA".into(), + }, + ), + ( + "RESOLVED IPV4 PREFIX", + status + .resolved_vpc_prefixes + .as_ref() + .and_then(|resolved| resolved.ipv4_vpc_prefix_id) + .map(|id| id.to_string().into()) + .unwrap_or_else(|| "NA".into()), + ), + ( + "RESOLVED IPV6 PREFIX", + status + .resolved_vpc_prefixes + .as_ref() + .and_then(|resolved| resolved.ipv6_vpc_prefix_id) + .map(|id| id.to_string().into()) + .unwrap_or_else(|| "NA".into()), + ), ( "MAC ADDR", status diff --git a/crates/agent/src/tests/full.rs b/crates/agent/src/tests/full.rs index 4526022a9d..f5147b83a0 100644 --- a/crates/agent/src/tests/full.rs +++ b/crates/agent/src/tests/full.rs @@ -815,6 +815,7 @@ async fn handle_netconf(AxumState(state): AxumState>>) -> impl device: None, device_instance: 0u32, vpc_id: None, + resolved_vpc_prefixes: None, }], configs_synced: rpc::SyncState::Synced.into(), }), diff --git a/crates/api-core/src/handlers/instance.rs b/crates/api-core/src/handlers/instance.rs index ee0a7a01c6..9681505d66 100644 --- a/crates/api-core/src/handlers/instance.rs +++ b/crates/api-core/src/handlers/instance.rs @@ -40,7 +40,7 @@ use model::dpa_interface::DpaSearchConfig; use model::instance::config::InstanceConfig; use model::instance::config::extension_services::InstanceExtensionServicesConfig; use model::instance::config::infiniband::InstanceInfinibandConfig; -use model::instance::config::network::{InstanceNetworkConfig, NetworkDetails}; +use model::instance::config::network::InstanceNetworkConfig; use model::instance::config::nvlink::InstanceNvLinkConfig; use model::instance::config::spx::InstanceSpxConfig; use model::instance::config::tenant_config::TenantConfig; @@ -1506,8 +1506,8 @@ async fn update_instance_network_config( // Copy the resources if same interface and network are mentioned. network.copy_existing_resources(&instance.config.network); - // Allocate network segment here if vpc_prefix_id is mentioned before validate. - allocate_network(network, txn).await?; + // Resolve prefix-backed network resources before validating the generated segment IDs. + allocate_network(network, &instance.config.tenant.tenant_organization_id, txn).await?; network .validate(allow_instance_vf) .map_err(CarbideError::from)?; @@ -1730,29 +1730,25 @@ pub async fn force_delete_instance( .new_config .interfaces .iter() - .filter_map(|x| match &x.network_details { - Some(NetworkDetails::VpcPrefixId(_)) => x.network_segment_id, - _ => None, - }) + .filter_map(|x| x.generated_network_segment_id()) .collect_vec(); network_segment_ids_with_vpc.extend( update_network_req .old_config .interfaces .iter() - .filter_map(|x| match &x.network_details { - Some(NetworkDetails::VpcPrefixId(_)) => x.network_segment_id, - _ => None, - }), + .filter_map(|x| x.generated_network_segment_id()), ); } - network_segment_ids_with_vpc.extend(instance.config.network.interfaces.iter().filter_map( - |x| match &x.network_details { - Some(NetworkDetails::VpcPrefixId(_)) => x.network_segment_id, - _ => None, - }, - )); + network_segment_ids_with_vpc.extend( + instance + .config + .network + .interfaces + .iter() + .filter_map(|x| x.generated_network_segment_id()), + ); let network_segments_set: std::collections::HashSet<::carbide_uuid::network::NetworkSegmentId> = network_segment_ids_with_vpc.drain(..).collect(); diff --git a/crates/api-core/src/instance/mod.rs b/crates/api-core/src/instance/mod.rs index f9000d29a3..69914fc5ad 100644 --- a/crates/api-core/src/instance/mod.rs +++ b/crates/api-core/src/instance/mod.rs @@ -16,7 +16,8 @@ */ use std::cmp::Ordering; -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque}; +use std::sync::Arc; use ::rpc::errors::RpcDataConversionError; use ::rpc::forge as rpc; @@ -25,6 +26,7 @@ use carbide_uuid::infiniband::IBPartitionId; use carbide_uuid::instance::InstanceId; use carbide_uuid::instance_type::InstanceTypeId; use carbide_uuid::machine::MachineId; +use carbide_uuid::network::NetworkSegmentId; use carbide_uuid::spx::SpxPartitionId; use carbide_uuid::vpc::{VpcId, VpcPrefixId}; use config_version::ConfigVersion; @@ -42,7 +44,8 @@ use model::instance::NewInstance; use model::instance::config::InstanceConfig; use model::instance::config::infiniband::InstanceInfinibandConfig; use model::instance::config::network::{ - InstanceNetworkConfig, InterfaceFunctionId, NetworkDetails, + InstanceInterfaceIpFamilyMode, InstanceNetworkConfig, InterfaceFunctionId, Ipv6InterfaceConfig, + NetworkDetails, }; use model::instance::config::spx::{InstanceSpxConfig, SpxAttachmentType}; use model::machine::machine_search_config::MachineSearchConfig; @@ -229,198 +232,916 @@ impl TryFrom for InstanceAllocationRequest { } } -/// Allocate network segment and update network segment id with it. -pub async fn allocate_network( - network_config: &mut InstanceNetworkConfig, +/// The initial candidate attempt plus at most two retries after an overlap conflict. +const PREFIX_ALLOCATION_TOTAL_ATTEMPTS: usize = 3; +const NETWORK_PREFIX_OVERLAP_CONSTRAINT: &str = "network_prefixes_prefix_excl"; + +/// Address-family key whose declaration order defines IPv4-before-IPv6 locking. +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +enum AllocationAddressFamily { + Ipv4, + Ipv6, +} + +impl AllocationAddressFamily { + /// Derives the family key from a network prefix. + fn from_network(prefix: IpNetwork) -> Self { + if prefix.is_ipv4() { + Self::Ipv4 + } else { + Self::Ipv6 + } + } + + /// Derives the family key from an individual address. + fn from_address(address: std::net::IpAddr) -> Self { + if address.is_ipv4() { + Self::Ipv4 + } else { + Self::Ipv6 + } + } + + /// Returns the point-to-point linknet length used for this family. + fn linknet_prefix(self) -> u8 { + match self { + Self::Ipv4 => 31, + Self::Ipv6 => 127, + } + } + + /// Returns the family name used in operator-facing errors. + fn name(self) -> &'static str { + match self { + Self::Ipv4 => "IPv4", + Self::Ipv6 => "IPv6", + } + } +} + +/// Identifies whether a prefix creates the segment or augments it for dual stack. +#[derive(Clone, Copy, Debug)] +enum PrefixAllocationSlot { + Primary, + SecondaryIpv6, +} + +/// Concrete database operation performed for one family allocation. +#[derive(Clone, Copy, Debug)] +enum PrefixAllocationOperation { + Create, + Attach(NetworkSegmentId), +} + +/// One unresolved interface-family allocation and its frozen candidate sequence. +#[derive(Clone, Debug)] +struct PrefixAllocationWork { + target_index: usize, + interface_index: usize, + vpc_id: VpcId, + family: AllocationAddressFamily, + candidates: Arc<[VpcPrefixId]>, + candidate_index: usize, + slot: PrefixAllocationSlot, + requested_prefix: Option, + automatic: bool, + saw_contention: bool, +} + +/// Mutable network configuration and its authorization tenant. +struct NetworkAllocationTarget<'a> { + network_config: &'a mut InstanceNetworkConfig, + tenant_organization_id: &'a TenantOrganizationId, +} + +/// Database-backed inputs used to plan prefix allocations without further awaits. +struct PrefixAllocationContext { + explicit_prefixes: HashMap, + automatic_candidates: BTreeMap<(VpcId, AllocationAddressFamily), Arc<[VpcPrefixId]>>, + vpcs: HashMap, +} + +/// Result of attempting one candidate within its bounded savepoint retries. +#[derive(Clone, Copy, Debug)] +enum CandidateAllocationOutcome { + Allocated { + network_segment_id: Option, + }, + Exhausted, + Deleted, + Contended, +} + +/// Reason a candidate must be skipped by every remaining work item in its group. +#[derive(Clone, Copy, Debug)] +enum CandidateUnavailable { + Exhausted, + Deleted, + Contended, +} + +/// Expands a caller's family mode in canonical IPv4-before-IPv6 order. +fn requested_families(mode: &InstanceInterfaceIpFamilyMode) -> &'static [AllocationAddressFamily] { + match mode { + InstanceInterfaceIpFamilyMode::Ipv4Only => &[AllocationAddressFamily::Ipv4], + InstanceInterfaceIpFamilyMode::Ipv6Only => &[AllocationAddressFamily::Ipv6], + InstanceInterfaceIpFamilyMode::DualStack => { + &[AllocationAddressFamily::Ipv4, AllocationAddressFamily::Ipv6] + } + } +} + +/// Returns whether an interface still needs generated prefix-backed resources. +fn interface_needs_prefix_allocation( + interface: &model::instance::config::network::InstanceInterfaceConfig, +) -> bool { + // Preserve the existing update contract: allocated addresses identify a + // reused interface, while an unallocated prefix-backed interface must be + // resolved even if a caller supplied a stale network_segment_id value. + interface.ip_addrs.is_empty() +} + +/// Checks the owning VPC's declared support for an address family. +fn vpc_supports_family( + virtualization_type: VpcVirtualizationType, + family: AllocationAddressFamily, +) -> bool { + match family { + AllocationAddressFamily::Ipv4 => virtualization_type.supports_ipv4_prefix(), + AllocationAddressFamily::Ipv6 => virtualization_type.supports_ipv6_prefix(), + } +} + +/// Identifies the one exclusion conflict that is safe to retry as contention. +fn is_network_prefix_overlap_conflict(error: &CarbideError) -> bool { + matches!( + error, + CarbideError::DBError(db::AnnotatedSqlxError { + source: sqlx::Error::Database(database_error), + .. + }) if database_error.constraint() == Some(NETWORK_PREFIX_OVERLAP_CONSTRAINT) + ) +} + +/// Performs one candidate allocation after the caller has established a savepoint. +async fn allocate_prefix_candidate_once( + txn: &mut PgConnection, + vpc_id: VpcId, + family: AllocationAddressFamily, + vpc_prefix_id: VpcPrefixId, + operation: PrefixAllocationOperation, + requested_prefix: Option, +) -> CarbideResult { + let Some(vpc_prefix) = db::vpc_prefix::lock_for_allocation(txn, vpc_prefix_id).await? else { + return Ok(CandidateAllocationOutcome::Deleted); + }; + + if vpc_prefix.vpc_id != vpc_id + || AllocationAddressFamily::from_network(vpc_prefix.config.prefix) != family + { + return Err(CarbideError::FailedPrecondition(format!( + "VPC prefix `{vpc_prefix_id}` no longer belongs to the requested {} allocation group in VPC `{vpc_id}`", + family.name(), + ))); + } + + let allocator = PrefixAllocator::new( + vpc_prefix.id, + vpc_prefix.config.prefix, + vpc_prefix.status.last_used_prefix, + family.linknet_prefix(), + )?; + let allocated_prefix = if let Some(requested_prefix) = requested_prefix { + allocator + .validate_desired_prefix(&mut *txn, requested_prefix) + .await?; + requested_prefix + } else { + match allocator.next_free_prefix(&mut *txn).await { + Ok(prefix) => prefix, + Err(CarbideError::ResourceExhausted(_)) => { + return Ok(CandidateAllocationOutcome::Exhausted); + } + Err(error) => return Err(error), + } + }; + let (network_segment_id, allocated_prefix) = match operation { + PrefixAllocationOperation::Create => { + let (network_segment_id, prefix) = allocator + .allocate_network_segment_for_prefix(&mut *txn, vpc_id, allocated_prefix) + .await?; + (Some(network_segment_id), prefix) + } + PrefixAllocationOperation::Attach(network_segment_id) => { + let prefix = allocator + .allocate_linknet_for_segment_with_prefix( + &mut *txn, + network_segment_id, + allocated_prefix, + ) + .await?; + (None, prefix) + } + }; + + db::vpc_prefix::update_last_used_prefix(&mut *txn, &vpc_prefix.id, allocated_prefix).await?; + + Ok(CandidateAllocationOutcome::Allocated { network_segment_id }) +} + +/// Attempts one candidate under a savepoint and classifies capacity or contention. +/// +/// Every unsuccessful attempt explicitly rolls back its savepoint before the +/// caller advances or retries, releasing any lock acquired only by that attempt. +async fn attempt_prefix_candidate( txn: &mut PgConnection, + vpc_id: VpcId, + family: AllocationAddressFamily, + vpc_prefix_id: VpcPrefixId, + operation: PrefixAllocationOperation, + requested_prefix: Option, +) -> CarbideResult { + for _ in 0..PREFIX_ALLOCATION_TOTAL_ATTEMPTS { + let mut savepoint = db::Transaction::begin_inner(txn).await?; + let allocation_result = allocate_prefix_candidate_once( + savepoint.as_pgconn(), + vpc_id, + family, + vpc_prefix_id, + operation, + requested_prefix, + ) + .await; + + match allocation_result { + Ok(outcome @ CandidateAllocationOutcome::Allocated { .. }) => { + savepoint.commit().await?; + return Ok(outcome); + } + Ok(CandidateAllocationOutcome::Deleted) => { + savepoint.rollback().await?; + return Ok(CandidateAllocationOutcome::Deleted); + } + Ok(CandidateAllocationOutcome::Exhausted) => { + savepoint.rollback().await?; + return Ok(CandidateAllocationOutcome::Exhausted); + } + Ok(CandidateAllocationOutcome::Contended) => { + savepoint.rollback().await?; + return Err(CarbideError::internal( + "candidate attempt produced an internal-only outcome".to_string(), + )); + } + Err(error) => { + let overlap_conflict = is_network_prefix_overlap_conflict(&error); + savepoint.rollback().await?; + + if overlap_conflict { + continue; + } + return Err(error); + } + } + } + + Ok(CandidateAllocationOutcome::Contended) +} + +/// Advances one work item monotonically or returns its final classified error. +fn advance_prefix_work( + work_index: usize, + current_candidate: VpcPrefixId, + unavailable: CandidateUnavailable, + works: &mut [PrefixAllocationWork], + pending: &mut BTreeMap>, ) -> CarbideResult<()> { - // Take ROW LEVEL lock on all the vpc_prefix taken. - // This is needed so that last_used_prefix is not modified by multiple clients at same time. - // Keep values in mut Hashmap and update last_used_prefix in the end of this function. - // Also Validate: - // 1. vpc_prefix_ids can span VPCs only when every VPC is FNN. - // 2. Pointed vpc'organization id must be same as instance's tenant_org. - // 3. If no vpc_prefix_id is mentioned, return. - - // Collect all VPC prefix IDs across all interfaces (supports both single and dual-stack). - let vpc_prefix_ids: Vec = network_config + let work = works.get_mut(work_index).ok_or_else(|| { + CarbideError::internal(format!( + "prefix allocation work index {work_index} is out of bounds", + )) + })?; + + if matches!(unavailable, CandidateUnavailable::Deleted) && !work.automatic { + return Err(CarbideError::InvalidArgument(format!( + "VPC prefix `{current_candidate}` is marked for deletion and cannot be used for allocation", + ))); + } + if matches!(unavailable, CandidateUnavailable::Contended) { + work.saw_contention = true; + } + + work.candidate_index += 1; + let Some(next_candidate) = work.candidates.get(work.candidate_index).copied() else { + let message = format!( + "no eligible {} VPC prefix in VPC `{}` could allocate an interface linknet", + work.family.name(), + work.vpc_id, + ); + return if work.saw_contention { + Err(CarbideError::UnavailableError(message)) + } else { + Err(CarbideError::ResourceExhausted(message)) + }; + }; + + if next_candidate <= current_candidate { + return Err(CarbideError::internal(format!( + "VPC prefix candidate order moved backward from `{current_candidate}` to `{next_candidate}`", + ))); + } + pending + .entry(next_candidate) + .or_default() + .push_back(work_index); + Ok(()) +} + +/// Persists a selected candidate into the interface's rolling-compatible fields. +fn apply_prefix_resolution( + targets: &mut [NetworkAllocationTarget<'_>], + work: &PrefixAllocationWork, + vpc_prefix_id: VpcPrefixId, + network_segment_id: Option, +) -> CarbideResult<()> { + let target = targets.get_mut(work.target_index).ok_or_else(|| { + CarbideError::internal(format!( + "network allocation target index {} is out of bounds", + work.target_index, + )) + })?; + let interface = target + .network_config .interfaces - .iter() - .flat_map(|x| { - let mut ids = Vec::new(); - if let Some(NetworkDetails::VpcPrefixId(id)) = x.network_details { - ids.push(id); + .get_mut(work.interface_index) + .ok_or_else(|| { + CarbideError::internal(format!( + "network interface index {} is out of bounds for allocation target {}", + work.interface_index, work.target_index, + )) + })?; + + match work.slot { + PrefixAllocationSlot::Primary => { + let network_segment_id = network_segment_id.ok_or_else(|| { + CarbideError::internal( + "primary VPC-prefix allocation did not create a network segment".to_string(), + ) + })?; + interface.network_segment_id = Some(network_segment_id); + interface.network_details = Some(NetworkDetails::VpcPrefixId(vpc_prefix_id)); + interface.vpc_id = Some(work.vpc_id); + } + PrefixAllocationSlot::SecondaryIpv6 => { + let requested_ip_addr = interface + .ipv6_interface_config + .as_ref() + .and_then(|config| config.requested_ip_addr); + interface.ipv6_interface_config = Some(Ipv6InterfaceConfig { + vpc_prefix_id, + requested_ip_addr, + }); + } + } + Ok(()) +} + +/// Loads the database-backed inputs needed to plan prefix allocations. +async fn load_prefix_allocation_context( + targets: &[NetworkAllocationTarget<'_>], + txn: &mut PgConnection, +) -> CarbideResult> { + let mut explicit_prefix_ids = BTreeSet::new(); + let mut automatic_vpc_ids = BTreeSet::new(); + let mut automatic_candidate_vpc_ids = BTreeSet::new(); + + for target in targets.iter() { + for interface in &target.network_config.interfaces { + if let Some(selection) = &interface.vpc_selection { + automatic_vpc_ids.insert(selection.vpc_id); + if interface_needs_prefix_allocation(interface) { + automatic_candidate_vpc_ids.insert(selection.vpc_id); + } + continue; } - if let Some(ref v6) = x.ipv6_interface_config { - ids.push(v6.vpc_prefix_id); + + if let Some(NetworkDetails::VpcPrefixId(vpc_prefix_id)) = + interface.network_details.as_ref() + { + explicit_prefix_ids.insert(*vpc_prefix_id); } - ids - }) - .collect_vec(); + if let Some(ipv6) = &interface.ipv6_interface_config { + explicit_prefix_ids.insert(ipv6.vpc_prefix_id); + } + } + } - if vpc_prefix_ids.is_empty() { - return Ok(()); + if explicit_prefix_ids.is_empty() && automatic_vpc_ids.is_empty() { + return Ok(None); } - let mut vpc_prefixes: HashMap = - db::vpc_prefix::get_by_id_with_row_lock(txn, &vpc_prefix_ids) - .await? - .iter() - .map(|x| (x.id, x.clone())) - .collect::>(); - - // This can be empty also if vpc_prefix_id is not configured at carbide. - // In this case error 'Unknown VPC prefix id' will be thrown. - let vpc_ids = vpc_prefixes - .values() - .map(|x| x.vpc_id) - .collect::>(); - if vpc_ids.len() > 1 { - let vpc_ids = vpc_ids.into_iter().collect_vec(); - let vpcs = db::vpc::find_by( - &mut *txn, - ObjectColumnFilter::List(db::vpc::IdColumn, &vpc_ids), - ) - .await?; + let explicit_prefix_ids = explicit_prefix_ids.into_iter().collect_vec(); + let explicit_prefixes = if explicit_prefix_ids.is_empty() { + Vec::new() + } else { + db::vpc_prefix::get_for_allocation_by_ids(txn, &explicit_prefix_ids).await? + }; + let explicit_prefixes: HashMap = explicit_prefixes + .into_iter() + .map(|prefix| (prefix.id, prefix)) + .collect(); - if vpcs.len() != vpc_ids.len() - || vpcs - .iter() - .any(|x| x.config.network_virtualization_type != VpcVirtualizationType::Fnn) - { - return Err(CarbideError::InvalidConfiguration( - ConfigValidationError::InvalidValue(format!( - "Interface config contains interfaces from multiple VPCs, which is only supported when all VPCs use FNN: prefixes={:?}, vpcs={:?}.", - vpc_prefixes - .values() - .map(|x| (x.id, x.vpc_id)) - .collect_vec(), - vpcs.iter() - .map(|x| (x.id, x.config.network_virtualization_type)) - .collect_vec() - )), - )); + for vpc_prefix_id in &explicit_prefix_ids { + let Some(vpc_prefix) = explicit_prefixes.get(vpc_prefix_id) else { + return Err(CarbideError::FailedPrecondition(format!( + "VPC prefix `{vpc_prefix_id}` does not exist", + ))); + }; + if vpc_prefix.is_marked_as_deleted() { + return Err(CarbideError::InvalidArgument(format!( + "VPC prefix `{vpc_prefix_id}` is marked for deletion and cannot be used for allocation", + ))); } } - // Allocate linknet prefixes for each interface's VPC prefix(es). - for interface in &mut network_config.interfaces { - // If IP address is already allocated, ignore. - // This is the case of updating network config when some - // interfaces already exist (adding/removing a VF). - if !interface.ip_addrs.is_empty() { - continue; + let automatic_candidate_vpc_ids = automatic_candidate_vpc_ids.into_iter().collect_vec(); + let automatic_prefixes = if automatic_candidate_vpc_ids.is_empty() { + Vec::new() + } else { + db::vpc_prefix::find_allocation_candidates(txn, &automatic_candidate_vpc_ids).await? + }; + let mut automatic_candidates: BTreeMap<(VpcId, AllocationAddressFamily), Vec> = + BTreeMap::new(); + for prefix in automatic_prefixes { + automatic_candidates + .entry(( + prefix.vpc_id, + AllocationAddressFamily::from_network(prefix.config.prefix), + )) + .or_default() + .push(prefix.id); + } + let automatic_candidates: BTreeMap<_, Arc<[VpcPrefixId]>> = automatic_candidates + .into_iter() + .map(|(group, candidates)| (group, Arc::from(candidates))) + .collect(); + + let mut referenced_vpc_ids = automatic_vpc_ids; + referenced_vpc_ids.extend(explicit_prefixes.values().map(|prefix| prefix.vpc_id)); + let referenced_vpc_ids = referenced_vpc_ids.into_iter().collect_vec(); + let vpcs = db::vpc::find_by( + &mut *txn, + ObjectColumnFilter::List(db::vpc::IdColumn, &referenced_vpc_ids), + ) + .await?; + let vpcs: HashMap = vpcs.into_iter().map(|vpc| (vpc.id, vpc)).collect(); + + for vpc_id in &referenced_vpc_ids { + if !vpcs.contains_key(vpc_id) { + return Err(CarbideError::FailedPrecondition(format!( + "VPC `{vpc_id}` does not exist", + ))); } + } - let Some(network_details) = &interface.network_details else { - continue; - }; + Ok(Some(PrefixAllocationContext { + explicit_prefixes, + automatic_candidates, + vpcs, + })) +} - match network_details { - NetworkDetails::NetworkSegment(_) => {} - NetworkDetails::VpcPrefixId(vpc_prefix_id) => { - let vpc_prefix_id = &VpcPrefixId::from(*vpc_prefix_id); - let (vpc_id, vpc_prefix, last_used_prefix) = { - vpc_prefixes - .get(vpc_prefix_id) - .map(|vpc| (vpc.vpc_id, vpc.config.prefix, vpc.status.last_used_prefix)) - .ok_or_else(|| { - CarbideError::internal(format!( - "Unknown VPC prefix id: {vpc_prefix_id}" - )) - })? - }; +/// Validates allocation intent and flattens it into canonical work items. +fn plan_prefix_allocations( + targets: &[NetworkAllocationTarget<'_>], + context: &PrefixAllocationContext, +) -> CarbideResult> { + let PrefixAllocationContext { + explicit_prefixes, + automatic_candidates, + vpcs, + } = context; + + let mut works = Vec::new(); + for (target_index, target) in targets.iter().enumerate() { + let mut target_vpc_ids = BTreeSet::new(); + + for (interface_index, interface) in target.network_config.interfaces.iter().enumerate() { + if let Some(selection) = &interface.vpc_selection { + let vpc = vpcs.get(&selection.vpc_id).ok_or_else(|| { + CarbideError::FailedPrecondition(format!( + "VPC `{}` does not exist", + selection.vpc_id, + )) + })?; + target_vpc_ids.insert(vpc.id); + + if vpc.config.tenant_organization_id != target.tenant_organization_id.to_string() { + return Err(CarbideError::FailedPrecondition(format!( + "VPC `{}` is not owned by Tenant `{}`", + vpc.id, target.tenant_organization_id, + ))); + } + if vpc.config.network_virtualization_type != VpcVirtualizationType::Fnn { + return Err(CarbideError::FailedPrecondition(format!( + "automatic VPC-prefix selection requires an FNN VPC; VPC `{}` uses {}", + vpc.id, vpc.config.network_virtualization_type, + ))); + } + + let families = requested_families(&selection.family_mode); + for family in families { + if !vpc_supports_family(vpc.config.network_virtualization_type, *family) { + return Err(CarbideError::FailedPrecondition(format!( + "VPC `{}` does not support {} prefixes", + vpc.id, + family.name(), + ))); + } + } + + // Resolved dual-stack intent legitimately retains an IPv6 + // sidecar for rolling compatibility, but automatic mode never + // accepts caller-selected addresses or an unrelated segment. + if interface.requested_ip_addr.is_some() + || interface + .ipv6_interface_config + .as_ref() + .is_some_and(|ipv6| ipv6.requested_ip_addr.is_some()) + || (interface.network_details.is_none() + && interface.ipv6_interface_config.is_some()) + || (selection.family_mode != InstanceInterfaceIpFamilyMode::DualStack + && interface.ipv6_interface_config.is_some()) + || matches!( + interface.network_details.as_ref(), + Some(NetworkDetails::NetworkSegment(_)) + ) + { + return Err(CarbideError::InvalidArgument( + "explicit IP requests, incompatible IPv6 configuration, and explicit network segments are invalid with automatic VPC-prefix selection" + .to_string(), + )); + } + if !interface_needs_prefix_allocation(interface) { + continue; + } - // Prevent dual-v6: if the primary VPC prefix is IPv6 and - // ipv6_interface_config is also set, we'd create two v6 linknets - // on the same segment. - if vpc_prefix.is_ipv6() && interface.ipv6_interface_config.is_some() { + for family in families { + let candidates = automatic_candidates + .get(&(selection.vpc_id, *family)) + .cloned() + .unwrap_or_else(|| Arc::from(Vec::::new())); + if candidates.is_empty() { + return Err(CarbideError::ResourceExhausted(format!( + "VPC `{}` has no eligible {} VPC prefix", + selection.vpc_id, + family.name(), + ))); + } + works.push(PrefixAllocationWork { + target_index, + interface_index, + vpc_id: selection.vpc_id, + family: *family, + candidates, + candidate_index: 0, + slot: if families.len() == 2 && *family == AllocationAddressFamily::Ipv6 { + PrefixAllocationSlot::SecondaryIpv6 + } else { + PrefixAllocationSlot::Primary + }, + requested_prefix: None, + automatic: true, + saw_contention: false, + }); + } + continue; + } + + let Some(NetworkDetails::VpcPrefixId(primary_prefix_id)) = + interface.network_details.as_ref() + else { + continue; + }; + let primary_prefix = explicit_prefixes.get(primary_prefix_id).ok_or_else(|| { + CarbideError::FailedPrecondition(format!( + "VPC prefix `{primary_prefix_id}` does not exist", + )) + })?; + let primary_vpc = vpcs.get(&primary_prefix.vpc_id).ok_or_else(|| { + CarbideError::FailedPrecondition(format!( + "VPC `{}` does not exist", + primary_prefix.vpc_id, + )) + })?; + target_vpc_ids.insert(primary_vpc.id); + if primary_vpc.config.tenant_organization_id + != target.tenant_organization_id.to_string() + { + return Err(CarbideError::FailedPrecondition(format!( + "VPC prefix `{primary_prefix_id}` belongs to VPC `{}`, which is not owned by Tenant `{}`", + primary_vpc.id, target.tenant_organization_id, + ))); + } + + let primary_family = + AllocationAddressFamily::from_network(primary_prefix.config.prefix); + if let Some(requested_ip_addr) = interface.requested_ip_addr + && AllocationAddressFamily::from_address(requested_ip_addr) != primary_family + { + return Err(CarbideError::InvalidArgument(format!( + "requested IP address `{requested_ip_addr}` does not match VPC prefix `{primary_prefix_id}`", + ))); + } + + let secondary_prefix = if let Some(ipv6) = &interface.ipv6_interface_config { + if primary_family != AllocationAddressFamily::Ipv4 { return Err(CarbideError::InvalidConfiguration( ConfigValidationError::InvalidValue( - "vpc_prefix_id points to an IPv6 prefix but ipv6_interface_config is also set -- use one or the other for IPv6".to_string(), + "vpc_prefix_id points to an IPv6 prefix but ipv6_interface_config is also set -- use one or the other for IPv6" + .to_string(), ), )); } + let prefix = explicit_prefixes.get(&ipv6.vpc_prefix_id).ok_or_else(|| { + CarbideError::FailedPrecondition(format!( + "VPC prefix `{}` does not exist", + ipv6.vpc_prefix_id, + )) + })?; + if !prefix.config.prefix.is_ipv6() { + return Err(CarbideError::InvalidArgument(format!( + "ipv6_interface_config VPC prefix `{}` is not IPv6", + ipv6.vpc_prefix_id, + ))); + } + if prefix.vpc_id != primary_prefix.vpc_id { + return Err(CarbideError::InvalidConfiguration( + ConfigValidationError::InvalidValue(format!( + "dual-stack VPC prefixes must belong to the same VPC: primary_vpc_prefix_id={primary_prefix_id}, primary_vpc_id={}, ipv6_vpc_prefix_id={}, ipv6_vpc_id={}", + primary_prefix.vpc_id, ipv6.vpc_prefix_id, prefix.vpc_id, + )), + )); + } + Some(prefix) + } else { + None + }; - let linknet_prefix = if vpc_prefix.is_ipv4() { 31 } else { 127 }; + if !interface_needs_prefix_allocation(interface) { + continue; + } - let requested_prefix = interface + works.push(PrefixAllocationWork { + target_index, + interface_index, + vpc_id: primary_prefix.vpc_id, + family: primary_family, + candidates: Arc::from(vec![*primary_prefix_id]), + candidate_index: 0, + slot: PrefixAllocationSlot::Primary, + requested_prefix: interface .requested_ip_addr - .map(|ip| build_requested_linknet_prefix(ip, linknet_prefix)) - .transpose()?; - - let allocator = PrefixAllocator::new( - *vpc_prefix_id, - vpc_prefix, - last_used_prefix, - linknet_prefix, - )?; - let (ns_id, prefix) = allocator - .allocate_network_segment(txn, vpc_id, requested_prefix) - .await?; - interface.network_segment_id = Some(ns_id); - vpc_prefixes.entry(*vpc_prefix_id).and_modify(|x| { - x.status.last_used_prefix = Some(prefix); + .map(|address| { + build_requested_linknet_prefix(address, primary_family.linknet_prefix()) + }) + .transpose()?, + automatic: false, + saw_contention: false, + }); + + if let Some(secondary_prefix) = secondary_prefix { + let ipv6 = interface.ipv6_interface_config.as_ref().ok_or_else(|| { + CarbideError::internal( + "validated IPv6 allocation lost its interface configuration".to_string(), + ) + })?; + works.push(PrefixAllocationWork { + target_index, + interface_index, + vpc_id: secondary_prefix.vpc_id, + family: AllocationAddressFamily::Ipv6, + candidates: Arc::from(vec![secondary_prefix.id]), + candidate_index: 0, + slot: PrefixAllocationSlot::SecondaryIpv6, + requested_prefix: ipv6 + .requested_ip_addr + .map(|address| { + build_requested_linknet_prefix( + std::net::IpAddr::V6(address), + AllocationAddressFamily::Ipv6.linknet_prefix(), + ) + }) + .transpose()?, + automatic: false, + saw_contention: false, }); + } + } + + if target_vpc_ids.len() > 1 + && target_vpc_ids.iter().any(|vpc_id| { + vpcs.get(vpc_id).is_none_or(|vpc| { + vpc.config.network_virtualization_type != VpcVirtualizationType::Fnn + }) + }) + { + return Err(CarbideError::InvalidConfiguration( + ConfigValidationError::InvalidValue(format!( + "Interface config contains prefix-backed interfaces from multiple VPCs, which is only supported when all VPCs use FNN: {:?}", + target_vpc_ids + .iter() + .filter_map(|vpc_id| { + vpcs.get(vpc_id) + .map(|vpc| (*vpc_id, vpc.config.network_virtualization_type)) + }) + .collect_vec(), + )), + )); + } + } + + Ok(works) +} + +/// Executes planned work in the canonical prefix-lock order. +async fn execute_prefix_allocations( + targets: &mut [NetworkAllocationTarget<'_>], + txn: &mut PgConnection, + mut works: Vec, +) -> CarbideResult<()> { + // IMPORTANT: Candidate order is part of the locking protocol. + // + // Prefix-backed batch work is grouped by VPC and address family, then + // candidate VPC prefixes are acquired in ascending VpcPrefixId order. + // Successful row locks live until the outer transaction commits. Changing + // this to caller order, utilization order, or re-ranking during allocation + // can invert locks between concurrent batches and introduce deadlocks. + // + // With a stable candidate set and auto-allocation callers, static first-fit + // also fills the lowest-ID parent prefix before advancing to the next. + // Blocking in this canonical order also lets Core re-read every candidate: + // RESOURCE_EXHAUSTED therefore means all candidates were proven exhausted. + // + // A future throughput-oriented policy could instead rank by utilization and + // use SKIP LOCKED, selecting the most-used currently unlocked prefix without + // candidate-lock wait cycles. That policy would spread allocations under + // contention and must return UNAVAILABLE, not RESOURCE_EXHAUSTED, whenever a + // skipped candidate leaves capacity unproven. Do not make that trade-off + // implicitly. Do not refresh or re-sort this transaction's candidate list. + let mut groups: BTreeMap<(VpcId, AllocationAddressFamily), Vec> = BTreeMap::new(); + for (work_index, work) in works.iter().enumerate() { + groups + .entry((work.vpc_id, work.family)) + .or_default() + .push(work_index); + } + + for (_, work_indices) in groups { + let mut pending: BTreeMap> = BTreeMap::new(); + for work_index in work_indices { + let work = works.get(work_index).ok_or_else(|| { + CarbideError::internal(format!( + "prefix allocation work index {work_index} is out of bounds", + )) + })?; + let candidate = work.candidates.first().copied().ok_or_else(|| { + CarbideError::internal(format!( + "prefix allocation work {work_index} has no candidates", + )) + })?; + pending.entry(candidate).or_default().push_back(work_index); + } + + while let Some((candidate, mut candidate_work)) = pending.pop_first() { + let mut unavailable = None; + while let Some(work_index) = candidate_work.pop_front() { + if let Some(unavailable) = unavailable { + advance_prefix_work( + work_index, + candidate, + unavailable, + &mut works, + &mut pending, + )?; + continue; + } - // Dual-stack: if IPv6 config is set, add a v6 linknet to the same segment. - if let Some(ref v6_config) = interface.ipv6_interface_config { - let v6_prefix_id = &v6_config.vpc_prefix_id; - let (v6_vpc_id, v6_vpc_prefix, v6_last_used) = { - vpc_prefixes - .get(v6_prefix_id) - .map(|vpc| (vpc.vpc_id, vpc.config.prefix, vpc.status.last_used_prefix)) + let work = works.get(work_index).cloned().ok_or_else(|| { + CarbideError::internal(format!( + "prefix allocation work index {work_index} is out of bounds", + )) + })?; + let operation = match work.slot { + PrefixAllocationSlot::Primary => PrefixAllocationOperation::Create, + PrefixAllocationSlot::SecondaryIpv6 => { + let target = targets.get(work.target_index).ok_or_else(|| { + CarbideError::internal(format!( + "network allocation target index {} is out of bounds", + work.target_index, + )) + })?; + let interface = target + .network_config + .interfaces + .get(work.interface_index) .ok_or_else(|| { CarbideError::internal(format!( - "Unknown VPC prefix id: {v6_prefix_id}" + "network interface index {} is out of bounds for allocation target {}", + work.interface_index, work.target_index, )) - })? - }; - - if v6_vpc_id != vpc_id { - return Err(CarbideError::InvalidConfiguration( - ConfigValidationError::InvalidValue(format!( - "dual-stack VPC prefixes must belong to the same VPC: primary_vpc_prefix_id={vpc_prefix_id}, primary_vpc_id={vpc_id}, ipv6_vpc_prefix_id={v6_prefix_id}, ipv6_vpc_id={v6_vpc_id}", - )), - )); + })?; + let network_segment_id = interface.network_segment_id.ok_or_else(|| { + CarbideError::internal( + "IPv6 allocation ran before its primary segment was created" + .to_string(), + ) + })?; + PrefixAllocationOperation::Attach(network_segment_id) } + }; - let v6_linknet_prefix = 127; - let v6_requested_prefix = v6_config - .requested_ip_addr - .map(|ipv6addr| { - build_requested_linknet_prefix( - std::net::IpAddr::V6(ipv6addr), - v6_linknet_prefix, - ) - }) - .transpose()?; - let v6_allocator = PrefixAllocator::new( - *v6_prefix_id, - v6_vpc_prefix, - v6_last_used, - v6_linknet_prefix, - )?; - let v6_prefix = v6_allocator - .allocate_linknet_for_segment(txn, ns_id, v6_requested_prefix) - .await?; - vpc_prefixes.entry(*v6_prefix_id).and_modify(|x| { - x.status.last_used_prefix = Some(v6_prefix); - }); + let outcome = attempt_prefix_candidate( + txn, + work.vpc_id, + work.family, + candidate, + operation, + work.requested_prefix, + ) + .await?; + match outcome { + CandidateAllocationOutcome::Allocated { network_segment_id } => { + apply_prefix_resolution(targets, &work, candidate, network_segment_id)? + } + CandidateAllocationOutcome::Exhausted => { + unavailable = Some(CandidateUnavailable::Exhausted); + advance_prefix_work( + work_index, + candidate, + CandidateUnavailable::Exhausted, + &mut works, + &mut pending, + )?; + } + CandidateAllocationOutcome::Deleted => { + unavailable = Some(CandidateUnavailable::Deleted); + advance_prefix_work( + work_index, + candidate, + CandidateUnavailable::Deleted, + &mut works, + &mut pending, + )?; + } + CandidateAllocationOutcome::Contended => { + unavailable = Some(CandidateUnavailable::Contended); + advance_prefix_work( + work_index, + candidate, + CandidateUnavailable::Contended, + &mut works, + &mut pending, + )?; + } } } } } - // Update last used prefixes here. - for vpc_prefix in vpc_prefixes.values() { - let Some(last_used_prefix) = vpc_prefix.status.last_used_prefix else { - continue; + Ok(()) +} + +/// Validates and allocates every prefix-backed target in one canonical lock order. +/// +/// The function flattens batch work before any generated resource is created so +/// caller order cannot influence the order in which prefix rows are locked. +async fn allocate_networks( + targets: &mut [NetworkAllocationTarget<'_>], + txn: &mut PgConnection, +) -> CarbideResult<()> { + let works = { + let Some(context) = load_prefix_allocation_context(targets, txn).await? else { + return Ok(()); }; - db::vpc_prefix::update_last_used_prefix(txn, &vpc_prefix.id, last_used_prefix).await?; - } + plan_prefix_allocations(targets, &context)? + }; + execute_prefix_allocations(targets, txn, works).await +} - Ok(()) +/// Allocates generated network resources for one instance network config. +pub async fn allocate_network( + network_config: &mut InstanceNetworkConfig, + tenant_organization_id: &TenantOrganizationId, + txn: &mut PgConnection, +) -> CarbideResult<()> { + allocate_networks( + &mut [NetworkAllocationTarget { + network_config, + tenant_organization_id, + }], + txn, + ) + .await } pub fn allocate_ib_port_guid( @@ -551,7 +1272,7 @@ pub async fn allocate_instance( /// 6. Load final instances, assemble snapshots, commit pub async fn batch_allocate_instances( api: &Api, - requests: Vec, + mut requests: Vec, host_health_config: HostHealthConfig, ) -> Result, CarbideError> { if requests.is_empty() { @@ -916,11 +1637,29 @@ pub async fn batch_allocate_instances( ) .await?; - // ==== Phase 5: Network allocation (sequential due to vpc_prefix tracking) ==== + // Resolve every prefix-backed interface in canonical lock order before + // restoring caller order for the remaining per-instance processing. + { + let mut network_allocation_targets = requests + .iter_mut() + .map(|request| { + let InstanceConfig { + network, tenant, .. + } = &mut request.config; + NetworkAllocationTarget { + network_config: network, + tenant_organization_id: &tenant.tenant_organization_id, + } + }) + .collect_vec(); + allocate_networks(&mut network_allocation_targets, &mut txn).await?; + } + + // ==== Phase 5: Network validation and per-instance processing ==== let mut processed_requests: Vec<(InstanceAllocationRequest, ManagedHostStateSnapshot)> = Vec::with_capacity(request_count); - for mut request in requests { + for request in requests { let machine_id = request.machine_id; let mh_snapshot = snapshot_map .remove(&machine_id) @@ -929,9 +1668,6 @@ pub async fn batch_allocate_instances( id: machine_id.to_string(), })?; - // Allocate network - allocate_network(&mut request.config.network, &mut txn).await?; - // Validate config (after network allocation sets network_segment_id) request.config.validate( true, diff --git a/crates/api-core/src/network_segment/allocate.rs b/crates/api-core/src/network_segment/allocate.rs index 0afb0dc6ee..46dc127111 100644 --- a/crates/api-core/src/network_segment/allocate.rs +++ b/crates/api-core/src/network_segment/allocate.rs @@ -47,6 +47,7 @@ fn u128_to_ip(val: u128, is_v6: bool) -> IpAddr { /// wrap_to_prefix_start returns `addr` if it falls within the VPC prefix, /// otherwise wraps around to the start of the VPC prefix. +#[cfg(test)] fn wrap_to_prefix_start(addr: IpAddr, vpc_prefix: &IpNetwork) -> IpAddr { let addr_u128 = ip_to_u128(addr); let network_u128 = ip_to_u128(vpc_prefix.network()); @@ -76,6 +77,7 @@ pub struct PrefixAllocator { /// given prefix length within a parent VPC prefix, using u128 arithmetic /// for both IPv4 and IPv6. #[derive(Debug)] +#[cfg(test)] struct PrefixIterator { vpc_prefix: IpNetwork, prefix: u8, @@ -92,7 +94,47 @@ fn networks_overlap(a: IpNetwork, b: IpNetwork) -> bool { a.contains(b.network()) || b.contains(a.network()) } +/// Returns the number of leading endpoints reserved on a generated linknet. +/// +/// IPv4 reserves its DPU endpoint through the explicit gateway. IPv6 cannot +/// persist a gateway, so reserve `::0` explicitly and allocate `::1` to the +/// instance. +fn generated_linknet_num_reserved(prefix: IpNetwork) -> i32 { + if prefix.is_ipv6() { 1 } else { 0 } +} + +/// Finds the first unoccupied linknet index in an inclusive search range. +/// +/// `occupied` must be sorted by its inclusive start index. Overlapping ranges +/// need not be merged because advancing past each range is monotonic. +fn first_unoccupied_index( + occupied: &[(u128, u128)], + range_start: u128, + range_end: u128, +) -> Option { + if range_start > range_end { + return None; + } + + let mut candidate = range_start; + for &(occupied_start, occupied_end) in occupied { + if occupied_end < candidate { + continue; + } + if occupied_start > candidate { + return Some(candidate); + } + + candidate = occupied_end.saturating_add(1); + if candidate > range_end { + return None; + } + } + Some(candidate) +} + impl PrefixAllocator { + #[cfg(test)] fn iter(&self) -> PrefixIterator { let is_v6 = self.vpc_prefix.is_ipv6(); @@ -147,8 +189,8 @@ impl PrefixAllocator { }) } - // As of now this is only used by FNN. - pub async fn allocate_network_segment( + #[cfg(test)] + pub(crate) async fn allocate_network_segment( &self, txn: &mut PgConnection, vpc_id: VpcId, @@ -161,6 +203,18 @@ impl PrefixAllocator { self.next_free_prefix(txn).await? }; + self.allocate_network_segment_for_prefix(txn, vpc_id, prefix) + .await + } + + /// Creates a generated segment for a prefix already selected and validated + /// by this allocator's caller. + pub(crate) async fn allocate_network_segment_for_prefix( + &self, + txn: &mut PgConnection, + vpc_id: VpcId, + prefix: IpNetwork, + ) -> CarbideResult<(NetworkSegmentId, IpNetwork)> { let name = format!("vpc_prefix_{}", prefix.network()); let segment_id = NetworkSegmentId::new(); @@ -183,7 +237,7 @@ impl PrefixAllocator { prefix, gateway, dhcpv6_link_address: None, - num_reserved: 0, + num_reserved: generated_linknet_num_reserved(prefix), }], vlan_id: None, vni: None, @@ -211,23 +265,14 @@ impl PrefixAllocator { Ok((segment.id, prefix)) } - /// Allocates a new linknet prefix from this VPC prefix and adds it to an existing - /// network segment. Used for dual-stack: the first prefix creates the segment via - /// `allocate_network_segment()`, and subsequent prefixes (e.g. the IPv6 counterpart) - /// are added to the same segment via this method. - pub async fn allocate_linknet_for_segment( + /// Attaches a prefix already selected and validated by this allocator's + /// caller to an existing generated segment. + pub(crate) async fn allocate_linknet_for_segment_with_prefix( &self, txn: &mut PgConnection, segment_id: NetworkSegmentId, - requested_prefix: Option, + prefix: IpNetwork, ) -> CarbideResult { - let prefix = if let Some(requested_prefix) = requested_prefix { - self.validate_desired_prefix(txn, requested_prefix).await?; - requested_prefix - } else { - self.next_free_prefix(txn).await? - }; - // IPv6 gateways are None (uses Router Advertisements). let gateway = if prefix.is_ipv4() { Some(prefix.network()) @@ -242,7 +287,7 @@ impl PrefixAllocator { prefix, gateway, dhcpv6_link_address: None, - num_reserved: 0, + num_reserved: generated_linknet_num_reserved(prefix), }], ) .await?; @@ -255,6 +300,10 @@ impl PrefixAllocator { Ok(prefix) } + /// Returns the next unoccupied child prefix using the persisted next-fit cursor. + /// + /// Occupied ranges are searched as linknet-index intervals so broad IPv6 + /// allocations do not require enumerating their constituent /127 prefixes. pub async fn next_free_prefix(&self, txn: &mut PgConnection) -> CarbideResult { let vpc_str = self.vpc_prefix.to_string(); let used_prefixes = db::network_prefix::containing_prefix(txn, vpc_str.as_str()) @@ -264,30 +313,84 @@ impl PrefixAllocator { .collect_vec(); // Reminder that `new()` already validated self.prefix > self.vpc_prefix.prefix(). - let total_network_possible: u128 = 1u128 << (self.prefix - self.vpc_prefix.prefix()) as u32; - let mut current_iteration: u128 = 0; - let mut allocator_itr = self.iter(); - - loop { - let Some(next_address) = allocator_itr.next() else { - return Err(CarbideError::internal("Prefix exhausted.".to_string())); - }; - - if !used_prefixes - .iter() - .any(|x| networks_overlap(*x, next_address)) - { - return Ok(next_address); - } - - if current_iteration > total_network_possible { - return Err(CarbideError::internal(format!( - "IP address exhausted: {}", + let prefix_delta = u32::from(self.prefix - self.vpc_prefix.prefix()); + let total_network_possible = 1u128.checked_shl(prefix_delta).ok_or_else(|| { + CarbideError::internal(format!( + "unable to represent the number of /{} linknets in VPC prefix {}", + self.prefix, self.vpc_prefix + )) + })?; + let max_bits = u32::from(self.vpc_prefix.address_family().interface_prefix_len()); + let host_bits = max_bits - u32::from(self.prefix); + let linknet_size = 1u128.checked_shl(host_bits).ok_or_else(|| { + CarbideError::internal(format!( + "unable to represent a /{} linknet address range", + self.prefix + )) + })?; + let vpc_start = ip_to_u128(self.vpc_prefix.network()); + let vpc_end = ip_to_u128(self.vpc_prefix.broadcast()); + let is_ipv6 = self.vpc_prefix.is_ipv6(); + + // Convert every overlapping network prefix into the inclusive range of + // linknet indexes that it occupies. This avoids enumerating enormous + // IPv6 spaces one /127 at a time when, for example, an existing /64 + // covers 2^63 possible linknets inside a /48 VPC prefix. + let mut occupied = used_prefixes + .into_iter() + .filter(|prefix| prefix.is_ipv6() == is_ipv6) + .map(|prefix| { + let occupied_start = ip_to_u128(prefix.network()).max(vpc_start); + let occupied_end = ip_to_u128(prefix.broadcast()).min(vpc_end); + ( + (occupied_start - vpc_start) / linknet_size, + (occupied_end - vpc_start) / linknet_size, + ) + }) + .collect_vec(); + occupied.sort_unstable(); + + // Preserve next-fit cursor semantics. An absent or out-of-range cursor + // starts at the parent prefix; the last linknet wraps to index zero. + let start_index = self + .last_used_prefix + .filter(|prefix| prefix.is_ipv6() == is_ipv6) + .and_then(|prefix| { + let prefix_end = ip_to_u128(prefix.broadcast()); + (vpc_start..=vpc_end) + .contains(&prefix_end) + .then(|| ((prefix_end - vpc_start) / linknet_size + 1) % total_network_possible) + }) + .unwrap_or_default(); + + let free_index = first_unoccupied_index(&occupied, start_index, total_network_possible - 1) + .or_else(|| { + start_index + .checked_sub(1) + .and_then(|range_end| first_unoccupied_index(&occupied, 0, range_end)) + }) + .ok_or_else(|| { + CarbideError::ResourceExhausted(format!( + "VPC prefix {} ({}) has no available /{} linknets", + self.vpc_prefix_id, self.vpc_prefix, self.prefix + )) + })?; + + let address = free_index + .checked_mul(linknet_size) + .and_then(|offset| vpc_start.checked_add(offset)) + .ok_or_else(|| { + CarbideError::internal(format!( + "allocated linknet index {free_index} overflowed VPC prefix {}", self.vpc_prefix - ))); - } - current_iteration += 1; - } + )) + })?; + IpNetwork::new(u128_to_ip(address, is_ipv6), self.prefix).map_err(|error| { + CarbideError::internal(format!( + "unable to construct allocated linknet in VPC prefix {}: {error}", + self.vpc_prefix + )) + }) } pub async fn validate_desired_prefix( @@ -326,6 +429,7 @@ impl PrefixAllocator { /// PrefixIterator is only constructed by `PrefixAllocator::iter`, which /// guarantees that `self.prefix` is a valid prefix length for the address /// family, meaning the `IpNetwork::new(...).unwrap()` calls below are safe. +#[cfg(test)] impl Iterator for PrefixIterator { type Item = IpNetwork; @@ -363,7 +467,29 @@ mod test { use ipnetwork::{IpNetwork, Ipv4Network, Ipv6Network}; - use crate::network_segment::allocate::PrefixAllocator; + use crate::network_segment::allocate::{PrefixAllocator, first_unoccupied_index}; + + /// Exercises interval lookup without enumerating every possible linknet. + #[test] + fn find_first_unoccupied_linknet_index() { + // These cases cover gaps, overlapping occupied ranges, and complete + // exhaustion without constructing an address space proportional to IPv6. + let cases = [ + ("empty", vec![], 0, 7, Some(0)), + ("leading occupied", vec![(0, 3)], 0, 7, Some(4)), + ("overlapping occupied", vec![(0, 3), (2, 6)], 0, 7, Some(7)), + ("gap", vec![(0, 1), (4, 7)], 0, 7, Some(2)), + ("exhausted", vec![(0, 7)], 0, 7, None), + ]; + + for (name, occupied, range_start, range_end, expected) in cases { + assert_eq!( + first_unoccupied_index(&occupied, range_start, range_end), + expected, + "case: {name}", + ); + } + } #[test] fn test_next_iter() { diff --git a/crates/api-core/src/tests/instance.rs b/crates/api-core/src/tests/instance.rs index 20b707efda..3867cb57e3 100644 --- a/crates/api-core/src/tests/instance.rs +++ b/crates/api-core/src/tests/instance.rs @@ -55,7 +55,8 @@ use model::dpu_machine_update::DpuMachineUpdate; use model::instance::config::extension_services::InstanceExtensionServicesConfig; use model::instance::config::infiniband::InstanceInfinibandConfig; use model::instance::config::network::{ - DeviceLocator, InstanceNetworkConfig, InterfaceFunctionId, NetworkDetails, + DeviceLocator, InstanceInterfaceIpFamilyMode, InstanceInterfaceVpcSelection, + InstanceNetworkConfig, InterfaceFunctionId, NetworkDetails, }; use model::instance::config::nvlink::InstanceNvLinkConfig; use model::instance::config::spx::InstanceSpxConfig; @@ -68,12 +69,18 @@ use model::machine::{ SpdmMeasuringState, ValidationState, }; use model::metadata::Metadata; +use model::network_prefix::NewNetworkPrefix; use model::network_security_group::NetworkSecurityGroupStatusObservation; -use model::network_segment::{NetworkSegmentSearchConfig, NetworkSegmentSearchFilter}; +use model::network_segment::{ + NetworkSegmentControllerState, NetworkSegmentSearchConfig, NetworkSegmentSearchFilter, + NetworkSegmentType, NewNetworkSegment, +}; +use model::tenant::TenantOrganizationId; use model::test_support::ManagedHostConfig; use model::vpc_prefix::VpcPrefixConfig; use rpc::forge::{ - DpuExtensionService, Issue, IssueCategory, ManagedHostQuarantineMode, TpmCaCert, TpmCaCertId, + AdminForceDeleteMachineRequest, DpuExtensionService, Issue, IssueCategory, + ManagedHostQuarantineMode, TpmCaCert, TpmCaCertId, }; use rpc::{InstanceReleaseRequest, InterfaceFunctionType, Timestamp}; use sqlx::PgPool; @@ -91,7 +98,7 @@ use crate::tests::common::api_fixtures::instance::{ }; use crate::tests::common::api_fixtures::rpc_instance::RpcInstance; use crate::tests::common::api_fixtures::{ - TestEnv, create_managed_host_multi_dpu, create_managed_host_with_ek, + TestEnv, TestManagedHost, create_managed_host_multi_dpu, create_managed_host_with_ek, remove_health_report_entry, send_health_report_entry, update_time_params, }; use crate::tests::common::attestation::spdm_attestation_run_to_failed_then_to_success; @@ -99,6 +106,14 @@ use crate::tests::common::rpc_builder::{ InstanceAllocationRequest, InstanceConfig, InstanceConfigExt, VpcCreationRequest, }; +/// Returns the tenant config that owns the shared VPC test fixture. +fn fixture_tenant_config() -> rpc::TenantConfig { + rpc::TenantConfig { + tenant_organization_id: FIXTURE_TENANT_ORG_ID.to_string(), + ..default_tenant_config() + } +} + pub async fn find_instances_by_label( env: &TestEnv, label: rpc::forge::Label, @@ -1582,6 +1597,7 @@ async fn test_instance_network_status_sync(_: PgPoolOptions, options: PgConnectO device: None, device_instance: 0u32, vpc_id: Some(vpc_id), + resolved_vpc_prefixes: None, }] ); @@ -1618,6 +1634,7 @@ async fn test_instance_network_status_sync(_: PgPoolOptions, options: PgConnectO device: None, device_instance: 0u32, vpc_id: Some(vpc_id), + resolved_vpc_prefixes: None, }] ); @@ -1660,6 +1677,7 @@ async fn test_instance_network_status_sync(_: PgPoolOptions, options: PgConnectO device: None, device_instance: 0u32, vpc_id: Some(vpc_id), + resolved_vpc_prefixes: None, }] ); @@ -1713,6 +1731,7 @@ async fn test_instance_network_status_sync(_: PgPoolOptions, options: PgConnectO device: None, device_instance: 0u32, vpc_id: Some(vpc_id), + resolved_vpc_prefixes: None, }] ); @@ -1751,6 +1770,7 @@ async fn test_instance_network_status_sync(_: PgPoolOptions, options: PgConnectO device: None, device_instance: 0u32, vpc_id: Some(vpc_id), + resolved_vpc_prefixes: None, }] ); @@ -2455,7 +2475,7 @@ async fn test_allocate_network_vpc_prefix_id(_: PgPoolOptions, options: PgConnec let config = rpc::InstanceConfig { tenant: Some(rpc::TenantConfig { - tenant_organization_id: "abc".to_string(), + tenant_organization_id: FIXTURE_TENANT_ORG_ID.to_string(), hostname: Some("xyz".to_string()), tenant_keyset_ids: vec![], }), @@ -2473,7 +2493,8 @@ async fn test_allocate_network_vpc_prefix_id(_: PgPoolOptions, options: PgConnec assert!(config.network.interfaces[0].network_segment_id.is_none()); let mut txn = env.db_txn().await; - allocate_network(&mut config.network, &mut txn) + let tenant_organization_id = config.tenant.tenant_organization_id.clone(); + allocate_network(&mut config.network, &tenant_organization_id, &mut txn) .await .unwrap(); @@ -2953,6 +2974,876 @@ async fn test_vpc_prefix_handling(pool: PgPool) { ) .await .unwrap_err(); + txn.rollback().await.unwrap(); + + // A /30 contains exactly two /31 linknets, making exhaustion deterministic. + let exhaustible_prefix = + IpNetwork::V4(Ipv4Network::new(Ipv4Addr::new(10, 217, 6, 0), 30).unwrap()); + let exhaustible_prefix_id = create_tenant_overlay_prefix_with_prefix( + &env, + vpc_id, + "exhaustible vpc prefix", + exhaustible_prefix, + ) + .await; + let allocator = + PrefixAllocator::new(exhaustible_prefix_id, exhaustible_prefix, None, 31).unwrap(); + let mut txn = env.db_txn().await; + for _ in 0..2 { + allocator + .allocate_network_segment(&mut txn, vpc_id, None) + .await + .unwrap(); + } + + // Capacity exhaustion must remain distinguishable from allocator defects. + let error = allocator + .allocate_network_segment(&mut txn, vpc_id, None) + .await + .unwrap_err(); + assert!(matches!(error, crate::CarbideError::ResourceExhausted(_))); +} + +/// Verifies static first-fit, family-complete resolution, and persisted RPC projection. +#[crate::sqlx_test] +async fn test_auto_vpc_prefix_selection_uses_static_first_fit(pool: PgPool) { + let fixture = create_auto_vpc_selection_fixture(pool).await; + + assert_static_ipv4_first_fit(&fixture).await; + assert_explicit_prefix_tenant_ownership(&fixture).await; + let allocated_instance = allocate_and_assert_auto_vpc_instance(&fixture).await; + assert_ipv4_candidates_exhausted(&fixture).await; + + let dual_stack_prefixes = add_dual_stack_prefix_capacity(&fixture).await; + assert_ipv6_only_resolution( + &fixture, + &allocated_instance, + dual_stack_prefixes.ipv6_prefix_id, + ) + .await; + assert_dual_stack_resolution(&fixture, &allocated_instance, &dual_stack_prefixes).await; +} + +/// Verifies an exclusion conflict rolls back its savepoint and retries the same candidate. +#[crate::sqlx_test] +async fn test_auto_vpc_prefix_selection_retries_concurrent_network_prefix_insert(pool: PgPool) { + let fixture = create_auto_vpc_selection_fixture(pool).await; + let conflicting_prefix = IpNetwork::new(fixture.lower_ipv4_prefix.network(), 31).unwrap(); + + // Keep a conflicting child prefix uncommitted so allocation cannot observe + // it before choosing the same linknet and waiting on the exclusion constraint. + let mut blocker = fixture.env.pool.begin().await.unwrap(); + let blocker_pid: i32 = sqlx::query_scalar("SELECT pg_backend_pid()") + .fetch_one(blocker.as_mut()) + .await + .unwrap(); + db::network_segment::persist( + NewNetworkSegment { + id: NetworkSegmentId::new(), + name: "overlap-contention-blocker".to_string(), + subdomain_id: None, + vpc_id: Some(fixture.vpc_id), + mtu: 9000, + prefixes: vec![NewNetworkPrefix { + prefix: conflicting_prefix, + gateway: Some(conflicting_prefix.network()), + dhcpv6_link_address: None, + num_reserved: 0, + }], + vlan_id: None, + vni: None, + segment_type: NetworkSegmentType::Tenant, + can_stretch: Some(false), + allocation_strategy: Default::default(), + }, + blocker.as_mut(), + NetworkSegmentControllerState::Ready, + ) + .await + .unwrap(); + + // Start allocation on another connection and prove its prefix insert is + // blocked by this exact transaction before allowing the conflict to resolve. + let allocation_task = tokio::spawn(allocate_automatic_ipv4_network( + fixture.env.pool.clone(), + fixture.vpc_id, + fixture.tenant_organization_id.clone(), + )); + wait_until_prefix_allocator_blocked_by( + &fixture.env.pool, + blocker_pid, + "INSERT INTO network_prefixes", + ) + .await; + blocker.commit().await.unwrap(); + + // The retry must stay on the same parent but choose the other free /31. + let allocated = allocation_task.await.unwrap().unwrap(); + let interface = &allocated.interfaces[0]; + assert_eq!( + interface.network_details, + Some(NetworkDetails::VpcPrefixId(fixture.lower_ipv4_prefix_id)), + ); + let mut txn = fixture.env.db_txn().await; + let generated_segment = db::network_segment::find_by( + txn.as_mut(), + ObjectColumnFilter::One(IdColumn, &interface.network_segment_id.unwrap()), + NetworkSegmentSearchConfig::default(), + ) + .await + .unwrap(); + assert_eq!(generated_segment.len(), 1); + assert_ne!(generated_segment[0].prefixes[0].prefix, conflicting_prefix); + assert!( + fixture + .lower_ipv4_prefix + .contains(generated_segment[0].prefixes[0].prefix.network()) + ); + txn.commit().await.unwrap(); +} + +/// Verifies a candidate deleted after discovery is re-read and skipped safely. +#[crate::sqlx_test] +async fn test_auto_vpc_prefix_selection_rechecks_concurrent_candidate_deletion(pool: PgPool) { + let fixture = create_auto_vpc_selection_fixture(pool).await; + + // Hold the lower candidate's soft-delete uncommitted. Candidate discovery + // still sees the prior active row, while its selected-row lock must wait. + let mut blocker = fixture.env.pool.begin().await.unwrap(); + let blocker_pid: i32 = sqlx::query_scalar("SELECT pg_backend_pid()") + .fetch_one(blocker.as_mut()) + .await + .unwrap(); + let _: VpcPrefixId = sqlx::query_scalar( + r#" + UPDATE network_vpc_prefixes + SET deleted = NOW() + WHERE id = $1 + AND deleted IS NULL + -- Hold the candidate row until allocation reaches its locking re-read. + RETURNING id + "#, + ) + .bind(fixture.lower_ipv4_prefix_id) + .fetch_one(blocker.as_mut()) + .await + .unwrap(); + + // Commit only after the allocator is waiting on the deleted candidate. + let allocation_task = tokio::spawn(allocate_automatic_ipv4_network( + fixture.env.pool.clone(), + fixture.vpc_id, + fixture.tenant_organization_id.clone(), + )); + wait_until_prefix_allocator_blocked_by(&fixture.env.pool, blocker_pid, "FOR NO KEY UPDATE") + .await; + blocker.commit().await.unwrap(); + + // The locking re-read must reject the deleted row and fall through. + let allocated = allocation_task.await.unwrap().unwrap(); + assert_eq!( + allocated.interfaces[0].network_details, + Some(NetworkDetails::VpcPrefixId(fixture.higher_ipv4_prefix_id)), + ); +} + +/// Verifies candidates inserted after discovery remain deferred to a later request. +#[crate::sqlx_test] +async fn test_auto_vpc_prefix_selection_freezes_concurrent_candidate_insert(pool: PgPool) { + let fixture = create_auto_vpc_selection_fixture(pool).await; + + // Exhaust both frozen candidates before arranging the discovery boundary. + assert_static_ipv4_first_fit(&fixture).await; + let final_original_allocation = allocate_automatic_ipv4_network( + fixture.env.pool.clone(), + fixture.vpc_id, + fixture.tenant_organization_id.clone(), + ) + .await + .unwrap(); + assert_eq!( + final_original_allocation.interfaces[0].network_details, + Some(NetworkDetails::VpcPrefixId(fixture.higher_ipv4_prefix_id)), + ); + + // Hold the first candidate so a blocked selected-row lock proves the + // allocation transaction already froze its candidate query result. + let mut blocker = fixture.env.pool.begin().await.unwrap(); + let blocker_pid: i32 = sqlx::query_scalar("SELECT pg_backend_pid()") + .fetch_one(blocker.as_mut()) + .await + .unwrap(); + let _: VpcPrefixId = sqlx::query_scalar( + r#" + SELECT id + FROM network_vpc_prefixes + WHERE id = $1 + -- Keep discovery unlocked but block the selected candidate re-read. + FOR UPDATE + "#, + ) + .bind(fixture.lower_ipv4_prefix_id) + .fetch_one(blocker.as_mut()) + .await + .unwrap(); + let allocation_task = tokio::spawn(allocate_automatic_ipv4_network( + fixture.env.pool.clone(), + fixture.vpc_id, + fixture.tenant_organization_id.clone(), + )); + wait_until_prefix_allocator_blocked_by(&fixture.env.pool, blocker_pid, "FOR NO KEY UPDATE") + .await; + + // Add usable capacity only after discovery, then let the frozen request continue. + let inserted_prefix_id = create_tenant_overlay_prefix_with_prefix( + &fixture.env, + fixture.vpc_id, + "concurrently-inserted-candidate", + IpNetwork::V4(Ipv4Network::new(Ipv4Addr::new(10, 218, 0, 8), 30).unwrap()), + ) + .await; + blocker.rollback().await.unwrap(); + + // The in-flight request proves only its original candidates exhausted; a + // fresh request discovers and allocates from the newly committed candidate. + let error = allocation_task.await.unwrap().unwrap_err(); + assert!(matches!(error, crate::CarbideError::ResourceExhausted(_))); + let retried = allocate_automatic_ipv4_network( + fixture.env.pool.clone(), + fixture.vpc_id, + fixture.tenant_organization_id.clone(), + ) + .await + .unwrap(); + assert_eq!( + retried.interfaces[0].network_details, + Some(NetworkDetails::VpcPrefixId(inserted_prefix_id)), + ); +} + +/// Verifies automatic VPC selection enforces ownership through the public RPC boundary. +#[crate::sqlx_test] +async fn test_auto_vpc_prefix_selection_rejects_cross_tenant_rpc_request(pool: PgPool) { + const OTHER_TENANT: &str = "auto-prefix-selection-other-tenant"; + + let fixture = create_auto_vpc_selection_fixture(pool).await; + create_fixture_tenant(&fixture.env, OTHER_TENANT) + .await + .unwrap(); + let managed_host = create_managed_host(&fixture.env).await; + + // Request the owning tenant's VPC from a different valid tenant through AllocateInstance. + let error = fixture + .env + .api + .allocate_instance( + InstanceAllocationRequest::builder(false) + .machine_id(managed_host.id) + .config( + InstanceConfig::default_tenant_and_os() + .tenant(rpc::TenantConfig { + tenant_organization_id: OTHER_TENANT.to_string(), + ..default_tenant_config() + }) + .network(automatic_ipv4_rpc_network_config(fixture.vpc_id)), + ) + .tonic_request(), + ) + .await + .unwrap_err(); + + // Ownership failure must retain its precise public status and diagnostic. + assert_eq!(error.code(), tonic::Code::FailedPrecondition); + assert_eq!( + error.message(), + format!( + "VPC `{}` is not owned by Tenant `{OTHER_TENANT}`", + fixture.vpc_id + ), + ); +} + +/// Verifies force deletion recognizes and cleans up an automatic selector's generated segment. +#[crate::sqlx_test] +async fn test_auto_vpc_prefix_selection_force_delete_marks_generated_segment_deleted(pool: PgPool) { + let fixture = create_auto_vpc_selection_fixture(pool).await; + let managed_host = create_managed_host(&fixture.env).await; + + // Allocate through the public selector and synchronize networking so the + // address being released is known to have become active. + let tinstance = managed_host + .instance_builer(&fixture.env) + .tenant_org(FIXTURE_TENANT_ORG_ID) + .network(automatic_ipv4_rpc_network_config(fixture.vpc_id)) + .build() + .await; + let instance_id = tinstance.id; + let persisted = tinstance.rpc_instance().await; + assert_ipv4_auto_rpc_resolution( + persisted.inner(), + fixture.vpc_id, + fixture.lower_ipv4_prefix_id, + ); + assert!( + !persisted.status().network().interfaces[0] + .addresses + .is_empty() + ); + let generated_segment_id = persisted.config().network().interfaces[0] + .network_segment_id + .unwrap(); + + // Force delete must route automatic intent through generated-resource cleanup. + let response = fixture + .env + .api + .admin_force_delete_machine(Request::new(AdminForceDeleteMachineRequest { + host_query: managed_host.id.to_string(), + delete_interfaces: false, + delete_bmc_interfaces: false, + delete_bmc_credentials: false, + allow_delete_with_orphaned_dpf_crds: false, + })) + .await + .unwrap() + .into_inner(); + assert!(response.all_done); + assert_eq!(response.instance_id, instance_id.to_string()); + assert!( + fixture + .env + .find_instances(vec![instance_id]) + .await + .instances + .is_empty() + ); + + // The generated segment is queued for lifecycle deletion and its instance address is freed. + let mut txn = fixture.env.db_txn().await; + let generated_segments = db::network_segment::find_by( + txn.as_mut(), + ObjectColumnFilter::One(IdColumn, &generated_segment_id), + NetworkSegmentSearchConfig::default(), + ) + .await + .unwrap(); + let [generated_segment] = generated_segments.as_slice() else { + panic!("expected one force-deleted generated segment"); + }; + assert!(generated_segment.is_marked_as_deleted()); + assert_eq!( + db::instance_address::count_by_segment_id(&mut txn, &generated_segment_id) + .await + .unwrap(), + 0, + ); + txn.commit().await.unwrap(); +} + +/// Shared resources for automatic VPC prefix-selection scenarios. +struct AutoVpcSelectionFixture { + env: TestEnv, + vpc_id: VpcId, + lower_ipv4_prefix_id: VpcPrefixId, + lower_ipv4_prefix: IpNetwork, + higher_ipv4_prefix_id: VpcPrefixId, + tenant_organization_id: TenantOrganizationId, +} + +/// Instance resources reused while exercising IPv6-only and dual-stack internals. +struct AutoVpcAllocatedInstance { + managed_host: TestManagedHost, + instance_id: InstanceId, +} + +/// Fresh per-family capacity used after exhausting the first-fit IPv4 prefixes. +struct AutoVpcDualStackPrefixes { + ipv4_prefix_id: VpcPrefixId, + ipv6_prefix_id: VpcPrefixId, +} + +/// Creates the VPC and ordered IPv4 prefix candidates used by the scenarios. +async fn create_auto_vpc_selection_fixture(pool: PgPool) -> AutoVpcSelectionFixture { + // The selected CIDRs do not overlap the default fixture networks, allowing + // the same environment to exercise both the allocator and a real instance. + let env = + create_test_env_with_overrides(pool, TestEnvOverrides::default().with_fnn_config(None)) + .await; + create_fixture_tenant(&env, FIXTURE_TENANT_ORG_ID) + .await + .unwrap(); + let vpc_id = env + .api + .create_vpc( + VpcCreationRequest::builder(FIXTURE_TENANT_ORG_ID) + .metadata(Metadata { + name: "auto-prefix-selection-vpc".to_string(), + ..Default::default() + }) + .network_virtualization_type(rpc::forge::VpcVirtualizationType::Fnn as i32) + .tonic_request(), + ) + .await + .unwrap() + .into_inner() + .id + .unwrap(); + let first_prefix = IpNetwork::V4(Ipv4Network::new(Ipv4Addr::new(10, 218, 0, 0), 30).unwrap()); + let first_prefix_id = create_tenant_overlay_prefix_with_prefix( + &env, + vpc_id, + "auto-prefix-candidate-one", + first_prefix, + ) + .await; + let second_prefix = IpNetwork::V4(Ipv4Network::new(Ipv4Addr::new(10, 218, 0, 4), 30).unwrap()); + let second_prefix_id = create_tenant_overlay_prefix_with_prefix( + &env, + vpc_id, + "auto-prefix-candidate-two", + second_prefix, + ) + .await; + let (lower_prefix_id, lower_prefix, higher_prefix_id) = if first_prefix_id < second_prefix_id { + (first_prefix_id, first_prefix, second_prefix_id) + } else { + (second_prefix_id, second_prefix, first_prefix_id) + }; + + AutoVpcSelectionFixture { + env, + vpc_id, + lower_ipv4_prefix_id: lower_prefix_id, + lower_ipv4_prefix: lower_prefix, + higher_ipv4_prefix_id: higher_prefix_id, + tenant_organization_id: FIXTURE_TENANT_ORG_ID.parse().unwrap(), + } +} + +/// Verifies deterministic first-fit selection and fallthrough to the next prefix. +async fn assert_static_ipv4_first_fit(fixture: &AutoVpcSelectionFixture) { + // Two /31 allocations fill the lower-ID /30; the third must fall through. + for expected_prefix_id in [ + fixture.lower_ipv4_prefix_id, + fixture.lower_ipv4_prefix_id, + fixture.higher_ipv4_prefix_id, + ] { + let mut network_config = + automatic_network_config(fixture.vpc_id, InstanceInterfaceIpFamilyMode::Ipv4Only); + let mut txn = fixture.env.db_txn().await; + allocate_network( + &mut network_config, + &fixture.tenant_organization_id, + &mut txn, + ) + .await + .unwrap(); + + // Resolution uses the rolling-compatible explicit fields internally. + let interface = &network_config.interfaces[0]; + assert_eq!( + interface.network_details, + Some(NetworkDetails::VpcPrefixId(expected_prefix_id)), + ); + assert!(interface.network_segment_id.is_some()); + txn.commit().await.unwrap(); + } +} + +/// Verifies explicit-prefix allocation enforces the same tenant boundary. +async fn assert_explicit_prefix_tenant_ownership(fixture: &AutoVpcSelectionFixture) { + // Explicit-prefix allocation enforces the same tenant ownership boundary. + let mut explicit_config = InstanceNetworkConfig::for_vpc_prefix_id( + fixture.higher_ipv4_prefix_id, + Some(fixture.vpc_id), + ); + let wrong_tenant_organization_id = "another-tenant".parse().unwrap(); + let mut txn = fixture.env.db_txn().await; + let error = allocate_network( + &mut explicit_config, + &wrong_tenant_organization_id, + &mut txn, + ) + .await + .unwrap_err(); + assert!(matches!( + error, + crate::CarbideError::FailedPrecondition(message) + if message.contains("is not owned by Tenant") + )); + txn.rollback().await.unwrap(); +} + +/// Allocates through the public request boundary and checks persisted projection. +async fn allocate_and_assert_auto_vpc_instance( + fixture: &AutoVpcSelectionFixture, +) -> AutoVpcAllocatedInstance { + // Allocate through the public IPv4 request boundary and verify both the + // immediate response and a subsequent FindInstancesByIds projection. + let managed_host = create_managed_host(&fixture.env).await; + let instance = fixture + .env + .api + .allocate_instance( + InstanceAllocationRequest::builder(false) + .machine_id(managed_host.id) + .config( + InstanceConfig::default_tenant_and_os() + .tenant(fixture_tenant_config()) + .network(automatic_ipv4_rpc_network_config(fixture.vpc_id)), + ) + .metadata(rpc::Metadata { + name: "automatic-vpc-prefix-selection".to_string(), + description: "tests/instance".to_string(), + labels: Vec::new(), + }) + .tonic_request(), + ) + .await + .unwrap() + .into_inner(); + assert_ipv4_auto_rpc_resolution(&instance, fixture.vpc_id, fixture.higher_ipv4_prefix_id); + + let instance_id = instance.id.unwrap(); + let persisted = fixture.env.one_instance(instance_id).await; + assert_ipv4_auto_rpc_resolution( + persisted.inner(), + fixture.vpc_id, + fixture.higher_ipv4_prefix_id, + ); + + AutoVpcAllocatedInstance { + managed_host, + instance_id, + } +} + +/// Verifies all original IPv4 candidates are exhausted after instance allocation. +async fn assert_ipv4_candidates_exhausted(fixture: &AutoVpcSelectionFixture) { + let mut exhausted_config = + automatic_network_config(fixture.vpc_id, InstanceInterfaceIpFamilyMode::Ipv4Only); + let mut txn = fixture.env.db_txn().await; + let error = allocate_network( + &mut exhausted_config, + &fixture.tenant_organization_id, + &mut txn, + ) + .await + .unwrap_err(); + assert!(matches!(error, crate::CarbideError::ResourceExhausted(_))); + txn.rollback().await.unwrap(); +} + +/// Adds one fresh prefix for each family after the first-fit checks complete. +async fn add_dual_stack_prefix_capacity( + fixture: &AutoVpcSelectionFixture, +) -> AutoVpcDualStackPrefixes { + // Add fresh family capacity after the IPv4 first-fit candidates have been + // exhausted, keeping the earlier ordering assertions deterministic. + let dual_ipv4_prefix_id = create_tenant_overlay_prefix_with_prefix( + &fixture.env, + fixture.vpc_id, + "dual-stack-ipv4-candidate", + IpNetwork::V4(Ipv4Network::new(Ipv4Addr::new(10, 218, 0, 8), 30).unwrap()), + ) + .await; + let ipv6_prefix = + IpNetwork::V6(Ipv6Network::new("fd42:218::".parse::().unwrap(), 126).unwrap()); + let ipv6_prefix_id = create_tenant_overlay_prefix_with_prefix( + &fixture.env, + fixture.vpc_id, + "automatic-ipv6-candidate", + ipv6_prefix, + ) + .await; + + AutoVpcDualStackPrefixes { + ipv4_prefix_id: dual_ipv4_prefix_id, + ipv6_prefix_id, + } +} + +/// Verifies internal IPv6-only resolution and odd-address assignment. +async fn assert_ipv6_only_resolution( + fixture: &AutoVpcSelectionFixture, + allocated_instance: &AutoVpcAllocatedInstance, + ipv6_prefix_id: VpcPrefixId, +) { + // IPv6-only stores its selected prefix in the legacy primary arm. + let mut ipv6_only_config = + automatic_network_config(fixture.vpc_id, InstanceInterfaceIpFamilyMode::Ipv6Only); + let mut txn = fixture.env.db_txn().await; + allocate_network( + &mut ipv6_only_config, + &fixture.tenant_organization_id, + &mut txn, + ) + .await + .unwrap(); + let ipv6_only_interface = &ipv6_only_config.interfaces[0]; + assert_eq!( + ipv6_only_interface.network_details, + Some(NetworkDetails::VpcPrefixId(ipv6_prefix_id)), + ); + assert!(ipv6_only_interface.ipv6_interface_config.is_none()); + let ipv6_only_segment_id = ipv6_only_interface.network_segment_id.unwrap(); + let ipv6_only_segment = db::network_segment::find_by( + txn.as_mut(), + ObjectColumnFilter::One(IdColumn, &ipv6_only_segment_id), + NetworkSegmentSearchConfig::default(), + ) + .await + .unwrap(); + assert_eq!(ipv6_only_segment[0].prefixes.len(), 1); + assert!(ipv6_only_segment[0].prefixes[0].prefix.is_ipv6()); + let host = allocated_instance + .managed_host + .host() + .db_machine(&mut txn) + .await; + ipv6_only_config = db::instance_network_config::with_allocated_ips( + ipv6_only_config, + txn.as_mut(), + allocated_instance.instance_id, + &host, + ) + .await + .unwrap(); + let ipv6_only_addresses = + db::instance_address::find_by_segment_id(txn.as_mut(), &ipv6_only_segment_id) + .await + .unwrap(); + assert_eq!(ipv6_only_config.interfaces[0].ip_addrs.len(), 1); + assert_eq!(ipv6_only_addresses.len(), 1); + assert!(matches!( + ipv6_only_addresses[0].address, + IpAddr::V6(address) if address.to_bits() & 1 == 1 + )); + txn.commit().await.unwrap(); +} + +/// Verifies internal dual-stack resolution and one address from each family. +async fn assert_dual_stack_resolution( + fixture: &AutoVpcSelectionFixture, + allocated_instance: &AutoVpcAllocatedInstance, + prefixes: &AutoVpcDualStackPrefixes, +) { + // Dual stack resolves IPv4 as primary and IPv6 as the secondary family on + // one generated segment, using the second /127 in the same IPv6 parent. + let mut dual_stack_config = + automatic_network_config(fixture.vpc_id, InstanceInterfaceIpFamilyMode::DualStack); + let mut txn = fixture.env.db_txn().await; + allocate_network( + &mut dual_stack_config, + &fixture.tenant_organization_id, + &mut txn, + ) + .await + .unwrap(); + let dual_stack_interface = &dual_stack_config.interfaces[0]; + assert_eq!( + dual_stack_interface.network_details, + Some(NetworkDetails::VpcPrefixId(prefixes.ipv4_prefix_id)), + ); + assert_eq!( + dual_stack_interface + .ipv6_interface_config + .as_ref() + .map(|config| config.vpc_prefix_id), + Some(prefixes.ipv6_prefix_id), + ); + let dual_stack_segment_id = dual_stack_interface.network_segment_id.unwrap(); + let dual_stack_segment = db::network_segment::find_by( + txn.as_mut(), + ObjectColumnFilter::One(IdColumn, &dual_stack_segment_id), + NetworkSegmentSearchConfig::default(), + ) + .await + .unwrap(); + assert_eq!(dual_stack_segment[0].prefixes.len(), 2); + assert_eq!( + dual_stack_segment[0] + .prefixes + .iter() + .filter(|prefix| prefix.prefix.is_ipv4()) + .count(), + 1, + ); + assert_eq!( + dual_stack_segment[0] + .prefixes + .iter() + .filter(|prefix| prefix.prefix.is_ipv6()) + .count(), + 1, + ); + let host = allocated_instance + .managed_host + .host() + .db_machine(&mut txn) + .await; + dual_stack_config = db::instance_network_config::with_allocated_ips( + dual_stack_config, + txn.as_mut(), + allocated_instance.instance_id, + &host, + ) + .await + .unwrap(); + let dual_stack_addresses = + db::instance_address::find_by_segment_id(txn.as_mut(), &dual_stack_segment_id) + .await + .unwrap(); + assert_eq!(dual_stack_config.interfaces[0].ip_addrs.len(), 2); + assert_eq!(dual_stack_addresses.len(), 2); + assert_eq!( + dual_stack_addresses + .iter() + .filter(|address| address.address.is_ipv4()) + .count(), + 1, + ); + assert!(dual_stack_addresses.iter().any(|address| matches!( + address.address, + IpAddr::V6(address) if address.to_bits() & 1 == 1 + ))); + txn.commit().await.unwrap(); +} + +/// Builds one unresolved internal automatic-VPC interface. +fn automatic_network_config( + vpc_id: VpcId, + family_mode: InstanceInterfaceIpFamilyMode, +) -> InstanceNetworkConfig { + let mut config = InstanceNetworkConfig::for_vpc_prefix_id(VpcPrefixId::new(), Some(vpc_id)); + let interface = &mut config.interfaces[0]; + interface.network_details = None; + interface.vpc_selection = Some(InstanceInterfaceVpcSelection { + vpc_id, + family_mode, + }); + config +} + +/// Allocates one automatic IPv4 config in its own transaction for concurrency tests. +async fn allocate_automatic_ipv4_network( + pool: PgPool, + vpc_id: VpcId, + tenant_organization_id: TenantOrganizationId, +) -> Result { + let mut config = automatic_network_config(vpc_id, InstanceInterfaceIpFamilyMode::Ipv4Only); + let mut txn = pool.begin().await.unwrap(); + let result = allocate_network(&mut config, &tenant_organization_id, txn.as_mut()).await; + + // Commit successful generated resources; failed attempts must leave no outer effects. + match result { + Ok(()) => { + txn.commit().await.unwrap(); + Ok(config) + } + Err(error) => { + txn.rollback().await.unwrap(); + Err(error) + } + } +} + +/// Waits until an allocator statement is blocked by the expected backend. +async fn wait_until_prefix_allocator_blocked_by( + pool: &PgPool, + blocker_pid: i32, + query_fragment: &str, +) { + for _ in 0..300 { + let blocked: bool = sqlx::query_scalar( + r#" + SELECT EXISTS ( + SELECT 1 + FROM pg_stat_activity AS activity + WHERE activity.datname = current_database() + AND activity.wait_event_type = 'Lock' + -- Match only allocator work blocked by this test's transaction. + AND $1 = ANY(pg_blocking_pids(activity.pid)) + AND activity.query ILIKE '%' || $2 || '%' + ) + "#, + ) + .bind(blocker_pid) + .bind(query_fragment) + .fetch_one(pool) + .await + .unwrap(); + if blocked { + return; + } + tokio::time::sleep(Duration::from_millis(100)).await; + } + + panic!("prefix allocator never blocked on {query_fragment}"); +} + +/// Builds one external IPv4 automatic-VPC interface request. +fn automatic_ipv4_rpc_network_config(vpc_id: VpcId) -> rpc::InstanceNetworkConfig { + rpc::InstanceNetworkConfig { + interfaces: vec![rpc::InstanceInterfaceConfig { + function_type: rpc::InterfaceFunctionType::Physical as i32, + network_segment_id: None, + network_details: Some(rpc::forge::instance_interface_config::NetworkDetails::Vpc( + rpc::forge::InstanceInterfaceVpcSelection { + vpc_id: Some(vpc_id), + family_mode: rpc::forge::InstanceInterfaceIpFamilyMode::Ipv4Only as i32, + }, + )), + device: None, + device_instance: 0, + virtual_function_id: None, + ip_address: None, + ipv6_interface_config: None, + routing_profile: None, + }], + #[allow(deprecated)] + auto: false, + auto_config: None, + } +} + +/// Verifies caller intent and active family-keyed resolution on an RPC instance. +fn assert_ipv4_auto_rpc_resolution( + instance: &rpc::Instance, + vpc_id: VpcId, + vpc_prefix_id: VpcPrefixId, +) { + let interface = &instance + .config + .as_ref() + .unwrap() + .network + .as_ref() + .unwrap() + .interfaces[0]; + let selection = match interface.network_details.as_ref() { + Some(rpc::forge::instance_interface_config::NetworkDetails::Vpc(selection)) => selection, + other => panic!("expected automatic VPC selection, got {other:?}"), + }; + assert_eq!(selection.vpc_id, Some(vpc_id)); + assert_eq!( + selection.family_mode, + rpc::forge::InstanceInterfaceIpFamilyMode::Ipv4Only as i32, + ); + assert!(interface.network_segment_id.is_some()); + + let status_interface = &instance + .status + .as_ref() + .unwrap() + .network + .as_ref() + .unwrap() + .interfaces[0]; + assert_eq!(status_interface.vpc_id, Some(vpc_id)); + let resolved = status_interface.resolved_vpc_prefixes.as_ref().unwrap(); + assert_eq!(resolved.ipv4_vpc_prefix_id, Some(vpc_prefix_id)); + assert_eq!(resolved.ipv6_vpc_prefix_id, None); } async fn create_tenant_overlay_prefix(env: &TestEnv, vpc_id: VpcId) -> VpcPrefixId { @@ -3580,7 +4471,7 @@ async fn test_network_details_migration( .allocate_instance(tonic::Request::new(rpc::forge::InstanceAllocationRequest { machine_id: mh_with_vpc_prefix.host_snapshot.id.into(), config: Some(rpc::InstanceConfig { - tenant: Some(default_tenant_config()), + tenant: Some(fixture_tenant_config()), os: Some(default_os_config()), network: Some(rpc::InstanceNetworkConfig { interfaces: vec![rpc::InstanceInterfaceConfig { @@ -4155,7 +5046,7 @@ async fn test_update_instance_config_vpc_prefix_network_update_delete_vf( }; let initial_config = rpc::InstanceConfig { - tenant: Some(default_tenant_config()), + tenant: Some(fixture_tenant_config()), os: Some(initial_os.clone()), network: Some(network.clone()), infiniband: None, @@ -4534,7 +5425,7 @@ async fn test_update_instance_config_vpc_prefix_network_update_state_machine( }; let initial_config = rpc::InstanceConfig { - tenant: Some(default_tenant_config()), + tenant: Some(fixture_tenant_config()), os: Some(initial_os.clone()), network: Some(network.clone()), infiniband: None, @@ -4758,7 +5649,7 @@ async fn test_allocate_network_multi_dpu_vpc_prefix_id( let config = rpc::InstanceConfig { tenant: Some(rpc::TenantConfig { - tenant_organization_id: "abc".to_string(), + tenant_organization_id: FIXTURE_TENANT_ORG_ID.to_string(), hostname: Some("xyz".to_string()), tenant_keyset_ids: vec![], }), @@ -4782,7 +5673,8 @@ async fn test_allocate_network_multi_dpu_vpc_prefix_id( ); let mut txn = env.db_txn().await; - allocate_network(&mut config.network, &mut txn) + let tenant_organization_id = config.tenant.tenant_organization_id.clone(); + allocate_network(&mut config.network, &tenant_organization_id, &mut txn) .await .unwrap(); @@ -4884,12 +5776,14 @@ async fn test_allocate_instance_with_multiple_fnn_vpc_prefixes( .allocate_instance( InstanceAllocationRequest::builder(false) .machine_id(mh.id) - .config(InstanceConfig::default_tenant_and_os().network( - dual_physical_network_config_with_vpc_prefixes( - first_prefix_id, - second_prefix_id, - ), - )) + .config( + InstanceConfig::default_tenant_and_os() + .tenant(fixture_tenant_config()) + .network(dual_physical_network_config_with_vpc_prefixes( + first_prefix_id, + second_prefix_id, + )), + ) .metadata(rpc::Metadata { name: "multi-fnn-vpc".to_string(), description: "tests/instance".to_string(), @@ -5319,9 +6213,11 @@ async fn test_allocate_instance_rejects_dual_stack_prefixes_from_different_vpcs( .allocate_instance( InstanceAllocationRequest::builder(false) .machine_id(mh.id) - .config(InstanceConfig::default_tenant_and_os().network( - rpc::InstanceNetworkConfig { - interfaces: vec![rpc::InstanceInterfaceConfig { + .config( + InstanceConfig::default_tenant_and_os() + .tenant(fixture_tenant_config()) + .network(rpc::InstanceNetworkConfig { + interfaces: vec![rpc::InstanceInterfaceConfig { function_type: rpc::InterfaceFunctionType::Physical as i32, network_segment_id: None, network_details: Some( @@ -5339,11 +6235,11 @@ async fn test_allocate_instance_rejects_dual_stack_prefixes_from_different_vpcs( }), routing_profile: None, }], - #[allow(deprecated)] - auto: false, - auto_config: None, - }, - )) + #[allow(deprecated)] + auto: false, + auto_config: None, + }), + ) .tonic_request(), ) .await diff --git a/crates/api-core/src/tests/instance_config_update.rs b/crates/api-core/src/tests/instance_config_update.rs index a28310c943..7a22e61997 100644 --- a/crates/api-core/src/tests/instance_config_update.rs +++ b/crates/api-core/src/tests/instance_config_update.rs @@ -18,9 +18,14 @@ use std::collections::HashMap; use carbide_uuid::network::NetworkSegmentId; -use common::api_fixtures::instance::{default_tenant_config, single_interface_network_config}; +use carbide_uuid::vpc::{VpcId, VpcPrefixId}; +use common::api_fixtures::instance::{ + TestInstance, default_os_config, default_tenant_config, single_interface_network_config, +}; +use common::api_fixtures::tenant::create_fixture_tenant; use common::api_fixtures::{ - TestEnvOverrides, create_managed_host, create_test_env, create_test_env_with_overrides, + TestEnv, TestEnvOverrides, TestManagedHost, create_managed_host, create_test_env, + create_test_env_with_overrides, }; use config_version::ConfigVersion; use rpc::forge::forge_server::Forge; @@ -29,6 +34,7 @@ use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use tonic::Request; use crate::cfg::file::{FnnConfig, FnnRoutingProfileConfig, PrefixFilterPolicyEntry}; +use crate::test_support::network_segment::FIXTURE_TENANT_ORG_ID; use crate::tests::common::api_fixtures::instance::advance_created_instance_into_ready_state; use crate::tests::common::api_fixtures::{create_managed_host_multi_dpu, get_vpc_fixture_id}; use crate::tests::common::rpc_builder::{ @@ -36,6 +42,14 @@ use crate::tests::common::rpc_builder::{ }; use crate::tests::common::{self}; +/// Returns the tenant config that owns the shared VPC test fixture. +fn fixture_tenant_config() -> rpc::TenantConfig { + rpc::TenantConfig { + tenant_organization_id: FIXTURE_TENANT_ORG_ID.to_string(), + ..default_tenant_config() + } +} + /// Compares an expected instance configuration with the actual instance configuration /// /// We can't directly call `assert_eq` since carbide will fill in details into various fields @@ -763,7 +777,7 @@ async fn test_update_instance_config_vpc_prefix_no_network_update( x.network_details = response.id.map(NetworkDetails::VpcPrefixId); }); let initial_config = rpc::InstanceConfig { - tenant: Some(default_tenant_config()), + tenant: Some(fixture_tenant_config()), os: Some(initial_os.clone()), network: Some(network.clone()), infiniband: None, @@ -844,6 +858,720 @@ async fn test_update_instance_config_vpc_prefix_no_network_update( ); } +/// VPC resources used by automatic-selector update scenarios. +#[derive(Clone, Copy)] +struct VpcPrefixFixture { + vpc_id: VpcId, + vpc_prefix_id: VpcPrefixId, +} + +/// Network resources that must survive the intent-only update. +struct ActiveVpcResources { + network_segment_id: NetworkSegmentId, + addresses: Vec, + internal_interface: model::instance::config::network::InstanceInterfaceConfig, +} + +/// Creates an FNN VPC and IPv4 prefix used by an update scenario. +async fn create_fnn_vpc_prefix_fixture( + env: &TestEnv, + tenant_organization_id: &str, + vpc_name: &str, + vpc_prefix_name: &str, + prefix: &str, +) -> VpcPrefixFixture { + let vpc_id = env + .api + .create_vpc( + VpcCreationRequest::builder(tenant_organization_id) + .metadata(rpc::Metadata { + name: vpc_name.to_string(), + ..Default::default() + }) + .network_virtualization_type(rpc::forge::VpcVirtualizationType::Fnn as i32) + .tonic_request(), + ) + .await + .unwrap() + .into_inner() + .id + .unwrap(); + let vpc_prefix_id = env + .api + .create_vpc_prefix(Request::new(rpc::forge::VpcPrefixCreationRequest { + id: None, + prefix: String::new(), + vpc_id: Some(vpc_id), + config: Some(rpc::forge::VpcPrefixConfig { + prefix: prefix.to_string(), + }), + metadata: Some(rpc::Metadata { + name: vpc_prefix_name.to_string(), + ..Default::default() + }), + })) + .await + .unwrap() + .into_inner() + .id + .unwrap(); + + VpcPrefixFixture { + vpc_id, + vpc_prefix_id, + } +} + +/// Builds a single-interface network configuration from caller intent. +fn single_vpc_interface_network(network_details: NetworkDetails) -> rpc::InstanceNetworkConfig { + rpc::InstanceNetworkConfig { + interfaces: vec![rpc::InstanceInterfaceConfig { + function_type: rpc::InterfaceFunctionType::Physical as i32, + network_segment_id: None, + network_details: Some(network_details), + device: None, + device_instance: 0, + virtual_function_id: None, + ip_address: None, + ipv6_interface_config: None, + routing_profile: None, + }], + #[allow(deprecated)] + auto: false, + auto_config: None, + } +} + +/// Builds automatic IPv4 VPC intent with an optional VF alongside the PF. +fn automatic_vpc_network( + vpc_id: VpcId, + include_virtual_function: bool, +) -> rpc::InstanceNetworkConfig { + // Start with PF intent, then mirror it for a Carbide-allocated VF when requested. + let selection = NetworkDetails::Vpc(rpc::forge::InstanceInterfaceVpcSelection { + vpc_id: Some(vpc_id), + family_mode: rpc::forge::InstanceInterfaceIpFamilyMode::Ipv4Only as i32, + }); + let mut network = single_vpc_interface_network(selection); + + if include_virtual_function { + let mut virtual_interface = network.interfaces[0].clone(); + virtual_interface.function_type = rpc::InterfaceFunctionType::Virtual as i32; + network.interfaces.push(virtual_interface); + } + + network +} + +/// Captures and validates the resources allocated for the explicit prefix. +async fn observe_active_vpc_resources( + env: &TestEnv, + tinstance: &TestInstance<'_, '_>, + fixture: VpcPrefixFixture, +) -> ActiveVpcResources { + let initial = tinstance.rpc_instance().await; + let initial_interface = &initial.config().network().interfaces[0]; + let network_segment_id = initial_interface.network_segment_id.unwrap(); + assert_eq!( + initial_interface.network_details, + Some(NetworkDetails::VpcPrefixId(fixture.vpc_prefix_id)), + ); + let initial_status_interface = &initial.status().network().interfaces[0]; + assert_eq!( + initial_status_interface + .resolved_vpc_prefixes + .as_ref() + .unwrap() + .ipv4_vpc_prefix_id, + Some(fixture.vpc_prefix_id), + ); + let addresses = initial_status_interface.addresses.clone(); + assert!(!addresses.is_empty()); + + let mut txn = env.pool.begin().await.unwrap(); + let initial_snapshot = tinstance.db_instance(&mut txn).await; + let internal_interface = initial_snapshot.config.network.interfaces[0].clone(); + txn.rollback().await.unwrap(); + assert_eq!( + internal_interface.network_segment_id, + Some(network_segment_id), + ); + assert_eq!(internal_interface.vpc_id, Some(fixture.vpc_id)); + + ActiveVpcResources { + network_segment_id, + addresses, + internal_interface, + } +} + +/// Stages automatic VPC intent and verifies the RPC response remains on active state. +async fn stage_automatic_vpc_update( + env: &TestEnv, + tinstance: &TestInstance<'_, '_>, + config: &rpc::InstanceConfig, + metadata: &rpc::Metadata, + fixture: VpcPrefixFixture, + active: &ActiveVpcResources, +) { + let response = env + .api + .update_instance_config( + InstanceConfigUpdateRequest::builder() + .instance_id(tinstance.id) + .config(config.clone()) + .metadata(metadata.clone()) + .tonic_request(), + ) + .await + .unwrap() + .into_inner(); + let response_interface = &response + .config + .as_ref() + .unwrap() + .network + .as_ref() + .unwrap() + .interfaces[0]; + assert_eq!( + response_interface.network_details, + Some(NetworkDetails::VpcPrefixId(fixture.vpc_prefix_id)), + ); + assert_eq!( + response_interface.network_segment_id, + Some(active.network_segment_id), + ); + let response_status_interface = &response + .status + .as_ref() + .unwrap() + .network + .as_ref() + .unwrap() + .interfaces[0]; + assert_eq!( + response_status_interface + .resolved_vpc_prefixes + .as_ref() + .unwrap() + .ipv4_vpc_prefix_id, + Some(fixture.vpc_prefix_id), + ); +} + +/// Verifies pending inventory keeps active allocations while observation status is pending. +async fn assert_pending_inventory_reuses_active_resources( + tinstance: &TestInstance<'_, '_>, + fixture: VpcPrefixFixture, + active: &ActiveVpcResources, +) { + let pending = tinstance.rpc_instance().await; + let pending_interface = &pending.config().network().interfaces[0]; + assert_eq!( + pending_interface.network_details, + Some(NetworkDetails::VpcPrefixId(fixture.vpc_prefix_id)), + ); + assert_eq!( + pending_interface.network_segment_id, + Some(active.network_segment_id), + ); + assert_eq!( + pending.status().network().interfaces[0] + .resolved_vpc_prefixes + .as_ref() + .unwrap() + .ipv4_vpc_prefix_id, + Some(fixture.vpc_prefix_id), + ); + let pending_status = pending.status().network(); + assert_eq!(pending_status.configs_synced(), rpc::SyncState::Pending); + assert!(pending_status.interfaces[0].addresses.is_empty()); +} + +/// Verifies the staged database request retains intent and all active allocations. +async fn assert_staged_automatic_vpc_request( + env: &TestEnv, + tinstance: &TestInstance<'_, '_>, + fixture: VpcPrefixFixture, + active: &ActiveVpcResources, +) { + let mut txn = env.pool.begin().await.unwrap(); + let pending_snapshot = tinstance.db_instance(&mut txn).await; + let pending_request = pending_snapshot + .update_network_config_request + .as_ref() + .unwrap(); + let staged_interface = &pending_request.new_config.interfaces[0]; + let staged_selection = staged_interface.vpc_selection.as_ref().unwrap(); + assert_eq!(staged_selection.vpc_id, fixture.vpc_id); + assert_eq!( + staged_selection.family_mode, + model::instance::config::network::InstanceInterfaceIpFamilyMode::Ipv4Only, + ); + assert_eq!( + staged_interface.generated_network_segment_id(), + Some(active.network_segment_id), + ); + assert_eq!( + staged_interface + .resolved_vpc_prefixes() + .unwrap() + .ipv4_vpc_prefix_id, + Some(fixture.vpc_prefix_id), + ); + assert_eq!( + staged_interface.ip_addrs, + active.internal_interface.ip_addrs, + ); + txn.rollback().await.unwrap(); +} + +/// Promotes the staged request and validates automatic intent and resource reuse. +async fn promote_automatic_vpc_request( + env: &TestEnv, + mh: &TestManagedHost, + tinstance: &TestInstance<'_, '_>, + fixture: VpcPrefixFixture, + active: &ActiveVpcResources, +) -> ConfigVersion { + env.run_machine_state_controller_iteration_network_config_return_to_ready(mh, false) + .await; + + let promoted = tinstance.rpc_instance().await; + let promoted_network_version = promoted.network_config_version(); + let promoted_interface = &promoted.config().network().interfaces[0]; + let promoted_selection = match promoted_interface.network_details.as_ref() { + Some(NetworkDetails::Vpc(selection)) => selection, + other => panic!("expected automatic VPC intent after promotion, got {other:?}"), + }; + assert_eq!(promoted_selection.vpc_id, Some(fixture.vpc_id)); + assert_eq!( + promoted_selection.family_mode, + rpc::forge::InstanceInterfaceIpFamilyMode::Ipv4Only as i32, + ); + assert_eq!( + promoted_interface.network_segment_id, + Some(active.network_segment_id), + ); + let promoted_status_interface = &promoted.status().network().interfaces[0]; + assert_eq!(promoted_status_interface.vpc_id, Some(fixture.vpc_id)); + assert_eq!(promoted_status_interface.addresses, active.addresses); + assert_eq!( + promoted_status_interface + .resolved_vpc_prefixes + .as_ref() + .unwrap() + .ipv4_vpc_prefix_id, + Some(fixture.vpc_prefix_id), + ); + + promoted_network_version +} + +/// Repeats the promoted selector and verifies the RPC response is idempotent. +async fn repeat_automatic_vpc_update( + env: &TestEnv, + tinstance: &TestInstance<'_, '_>, + config: &rpc::InstanceConfig, + metadata: &rpc::Metadata, + fixture: VpcPrefixFixture, + active: &ActiveVpcResources, + promoted_network_version: &ConfigVersion, +) { + let repeated = env + .api + .update_instance_config( + InstanceConfigUpdateRequest::builder() + .instance_id(tinstance.id) + .config(config.clone()) + .metadata(metadata.clone()) + .tonic_request(), + ) + .await + .unwrap() + .into_inner(); + assert_eq!( + repeated.network_config_version, + promoted_network_version.to_string(), + ); + let repeated_interface = &repeated + .config + .as_ref() + .unwrap() + .network + .as_ref() + .unwrap() + .interfaces[0]; + let repeated_selection = match repeated_interface.network_details.as_ref() { + Some(NetworkDetails::Vpc(selection)) => selection, + other => panic!("expected repeated automatic VPC intent, got {other:?}"), + }; + assert_eq!(repeated_selection.vpc_id, Some(fixture.vpc_id)); + assert_eq!( + repeated_selection.family_mode, + rpc::forge::InstanceInterfaceIpFamilyMode::Ipv4Only as i32, + ); + assert_eq!( + repeated_interface.network_segment_id, + Some(active.network_segment_id), + ); +} + +/// Verifies the repeated request leaves the database and generated segment unchanged. +async fn assert_repeated_update_reuses_active_resources( + env: &TestEnv, + tinstance: &TestInstance<'_, '_>, + fixture: VpcPrefixFixture, + active: &ActiveVpcResources, + promoted_network_version: &ConfigVersion, +) { + let mut txn = env.pool.begin().await.unwrap(); + let repeated_snapshot = tinstance.db_instance(&mut txn).await; + assert!(repeated_snapshot.update_network_config_request.is_none()); + assert_eq!( + &repeated_snapshot.network_config_version, + promoted_network_version, + ); + let repeated_interface = &repeated_snapshot.config.network.interfaces[0]; + assert_eq!( + repeated_interface.generated_network_segment_id(), + Some(active.network_segment_id), + ); + assert_eq!( + repeated_interface + .resolved_vpc_prefixes() + .unwrap() + .ipv4_vpc_prefix_id, + Some(fixture.vpc_prefix_id), + ); + assert_eq!( + repeated_interface.ip_addrs, + active.internal_interface.ip_addrs, + ); + let reused_segments = db::network_segment::find_by( + txn.as_mut(), + db::ObjectColumnFilter::One(db::network_segment::IdColumn, &active.network_segment_id), + Default::default(), + ) + .await + .unwrap(); + let [reused_segment] = reused_segments.as_slice() else { + panic!("expected the reused generated network segment to remain present"); + }; + assert!(!reused_segment.is_marked_as_deleted()); + assert_eq!(reused_segment.prefixes.len(), 1); + assert_eq!( + reused_segment.prefixes[0].vpc_prefix_id, + Some(fixture.vpc_prefix_id), + ); + txn.rollback().await.unwrap(); +} + +/// Verifies explicit-to-automatic intent staging, promotion, reuse, and idempotency. +#[crate::sqlx_test] +async fn test_update_explicit_vpc_prefix_to_automatic_vpc_reuses_active_resources( + _: PgPoolOptions, + options: PgConnectOptions, +) { + let pool = PgPoolOptions::new().connect_with(options).await.unwrap(); + let tenant = default_tenant_config(); + let env = + create_test_env_with_overrides(pool, TestEnvOverrides::default().with_fnn_config(None)) + .await; + create_fixture_tenant(&env, tenant.tenant_organization_id.clone()) + .await + .unwrap(); + let fixture = create_fnn_vpc_prefix_fixture( + &env, + tenant.tenant_organization_id.as_str(), + "explicit-to-automatic-vpc", + "explicit-to-automatic-prefix", + "192.1.4.0/25", + ) + .await; + let mh = create_managed_host(&env).await; + let metadata = rpc::Metadata { + name: "explicit-to-automatic-instance".to_string(), + description: "tests/instance_config_update".to_string(), + labels: Vec::new(), + }; + let initial_network = + single_vpc_interface_network(NetworkDetails::VpcPrefixId(fixture.vpc_prefix_id)); + let initial_config = rpc::InstanceConfig { + tenant: Some(tenant), + os: Some(default_os_config()), + network: Some(initial_network), + infiniband: None, + network_security_group_id: None, + dpu_extension_services: None, + nvlink: None, + spxconfig: None, + }; + let tinstance = mh + .instance_builer(&env) + .config(initial_config.clone()) + .metadata(metadata.clone()) + .build() + .await; + let active = observe_active_vpc_resources(&env, &tinstance, fixture).await; + + let automatic_network = single_vpc_interface_network(NetworkDetails::Vpc( + rpc::forge::InstanceInterfaceVpcSelection { + vpc_id: Some(fixture.vpc_id), + family_mode: rpc::forge::InstanceInterfaceIpFamilyMode::Ipv4Only as i32, + }, + )); + let mut automatic_config = initial_config; + automatic_config.network = Some(automatic_network); + + stage_automatic_vpc_update( + &env, + &tinstance, + &automatic_config, + &metadata, + fixture, + &active, + ) + .await; + assert_pending_inventory_reuses_active_resources(&tinstance, fixture, &active).await; + assert_staged_automatic_vpc_request(&env, &tinstance, fixture, &active).await; + let promoted_network_version = + promote_automatic_vpc_request(&env, &mh, &tinstance, fixture, &active).await; + repeat_automatic_vpc_update( + &env, + &tinstance, + &automatic_config, + &metadata, + fixture, + &active, + &promoted_network_version, + ) + .await; + assert_repeated_update_reuses_active_resources( + &env, + &tinstance, + fixture, + &active, + &promoted_network_version, + ) + .await; +} + +/// Verifies automatic VPC replacement, interface removal, and final deletion cleanup. +#[crate::sqlx_test] +async fn test_automatic_vpc_update_and_interface_removal_cleanup( + _: PgPoolOptions, + options: PgConnectOptions, +) { + // Create two eligible FNN VPCs so the update must replace generated resources. + let pool = PgPoolOptions::new().connect_with(options).await.unwrap(); + let tenant = default_tenant_config(); + let env = + create_test_env_with_overrides(pool, TestEnvOverrides::default().with_fnn_config(None)) + .await; + create_fixture_tenant(&env, tenant.tenant_organization_id.clone()) + .await + .unwrap(); + let first_vpc = create_fnn_vpc_prefix_fixture( + &env, + tenant.tenant_organization_id.as_str(), + "automatic-cleanup-vpc-a", + "automatic-cleanup-prefix-a", + "192.1.4.0/25", + ) + .await; + let second_vpc = create_fnn_vpc_prefix_fixture( + &env, + tenant.tenant_organization_id.as_str(), + "automatic-cleanup-vpc-b", + "automatic-cleanup-prefix-b", + "192.0.5.0/25", + ) + .await; + let mh = create_managed_host(&env).await; + let metadata = rpc::Metadata { + name: "automatic-vpc-cleanup-instance".to_string(), + description: "tests/instance_config_update".to_string(), + labels: Vec::new(), + }; + let initial_config = rpc::InstanceConfig { + tenant: Some(tenant), + os: Some(default_os_config()), + network: Some(automatic_vpc_network(first_vpc.vpc_id, true)), + infiniband: None, + network_security_group_id: None, + dpu_extension_services: None, + nvlink: None, + spxconfig: None, + }; + + // Allocate a PF and VF from VPC A, then capture their persisted generated resources. + let tinstance = mh + .instance_builer(&env) + .config(initial_config.clone()) + .metadata(metadata.clone()) + .build() + .await; + let active = tinstance.rpc_instance().await; + let active_config = active.config(); + let active_interfaces = &active_config.network().interfaces; + assert_eq!(active_interfaces.len(), 2); + let old_segment_ids = active_interfaces + .iter() + .map(|interface| interface.network_segment_id.unwrap()) + .collect::>(); + assert_ne!(old_segment_ids[0], old_segment_ids[1]); + assert!(active_interfaces.iter().all(|interface| { + matches!( + interface.network_details.as_ref(), + Some(NetworkDetails::Vpc(selection)) if selection.vpc_id == Some(first_vpc.vpc_id) + ) + })); + let active_status_interfaces = &active.status().network().interfaces; + assert_eq!(active_status_interfaces.len(), 2); + assert!(active_status_interfaces.iter().all(|interface| { + interface + .resolved_vpc_prefixes + .as_ref() + .is_some_and(|resolved| { + resolved.ipv4_vpc_prefix_id == Some(first_vpc.vpc_prefix_id) + && resolved.ipv6_vpc_prefix_id.is_none() + }) + })); + + let mut txn = env.db_txn().await; + for segment_id in &old_segment_ids { + assert_eq!( + db::instance_address::find_by_segment_id(txn.as_mut(), segment_id) + .await + .unwrap() + .len(), + 1, + ); + } + txn.rollback().await.unwrap(); + + // Replace the PF with VPC B intent and omit the VF from the staged configuration. + let mut updated_config = initial_config; + updated_config.network = Some(automatic_vpc_network(second_vpc.vpc_id, false)); + env.api + .update_instance_config( + InstanceConfigUpdateRequest::builder() + .instance_id(tinstance.id) + .config(updated_config) + .metadata(metadata) + .tonic_request(), + ) + .await + .unwrap(); + env.run_machine_state_controller_iteration_network_config_return_to_ready(&mh, true) + .await; + + // Verify inventory exposes only the new VPC B PF after controller promotion. + let promoted = tinstance.rpc_instance().await; + let promoted_config = promoted.config(); + let [promoted_interface] = promoted_config.network().interfaces.as_slice() else { + panic!("expected exactly one promoted automatic interface"); + }; + let Some(NetworkDetails::Vpc(promoted_selection)) = promoted_interface.network_details.as_ref() + else { + panic!("expected promoted automatic VPC intent"); + }; + assert_eq!(promoted_selection.vpc_id, Some(second_vpc.vpc_id)); + assert_eq!( + promoted_interface.function_type, + rpc::InterfaceFunctionType::Physical as i32, + ); + let new_segment_id = promoted_interface.network_segment_id.unwrap(); + assert!(!old_segment_ids.contains(&new_segment_id)); + let promoted_instance_status = promoted.status(); + let [promoted_status] = promoted_instance_status.network().interfaces.as_slice() else { + panic!("expected exactly one promoted automatic interface status"); + }; + assert_eq!(promoted_status.addresses.len(), 1); + assert_eq!( + promoted_status + .resolved_vpc_prefixes + .as_ref() + .unwrap() + .ipv4_vpc_prefix_id, + Some(second_vpc.vpc_prefix_id), + ); + + // Old A resources must be released while the new B segment remains active. + let mut txn = env.db_txn().await; + let promoted_snapshot = tinstance.db_instance(&mut txn).await; + assert!(promoted_snapshot.update_network_config_request.is_none()); + let old_segments = db::network_segment::find_by( + txn.as_mut(), + db::ObjectColumnFilter::List(db::network_segment::IdColumn, &old_segment_ids), + Default::default(), + ) + .await + .unwrap(); + assert_eq!(old_segments.len(), 2); + assert!( + old_segments + .iter() + .all(|segment| segment.is_marked_as_deleted()) + ); + for segment_id in &old_segment_ids { + assert!( + db::instance_address::find_by_segment_id(txn.as_mut(), segment_id) + .await + .unwrap() + .is_empty() + ); + } + let new_segments = db::network_segment::find_by( + txn.as_mut(), + db::ObjectColumnFilter::One(db::network_segment::IdColumn, &new_segment_id), + Default::default(), + ) + .await + .unwrap(); + let [new_segment] = new_segments.as_slice() else { + panic!("expected the promoted VPC B segment to remain present"); + }; + assert!(!new_segment.is_marked_as_deleted()); + assert_eq!( + new_segment.prefixes[0].vpc_prefix_id, + Some(second_vpc.vpc_prefix_id) + ); + assert_eq!( + db::instance_address::find_by_segment_id(txn.as_mut(), &new_segment_id) + .await + .unwrap() + .len(), + 1, + ); + txn.rollback().await.unwrap(); + + // Normal instance deletion must remove every generated segment and address. + tinstance.delete().await; + let mut generated_segment_ids = old_segment_ids; + generated_segment_ids.push(new_segment_id); + let mut txn = env.db_txn().await; + let remaining_segments = db::network_segment::find_by( + txn.as_mut(), + db::ObjectColumnFilter::List(db::network_segment::IdColumn, &generated_segment_ids), + Default::default(), + ) + .await + .unwrap(); + assert!(remaining_segments.is_empty()); + for segment_id in &generated_segment_ids { + assert!( + db::instance_address::find_by_segment_id(txn.as_mut(), segment_id) + .await + .unwrap() + .is_empty() + ); + } + txn.rollback().await.unwrap(); +} + #[crate::sqlx_test] async fn test_update_instance_config_vpc_prefix_network_update( _: PgPoolOptions, @@ -908,7 +1636,7 @@ async fn test_update_instance_config_vpc_prefix_network_update( }; let initial_config = rpc::InstanceConfig { - tenant: Some(default_tenant_config()), + tenant: Some(fixture_tenant_config()), os: Some(initial_os.clone()), network: Some(network.clone()), infiniband: None, @@ -1122,7 +1850,7 @@ async fn test_update_instance_config_vpc_prefix_network_update_post_instance_del }; let initial_config = rpc::InstanceConfig { - tenant: Some(default_tenant_config()), + tenant: Some(fixture_tenant_config()), os: Some(initial_os.clone()), network: Some(network.clone()), infiniband: None, @@ -1284,7 +2012,7 @@ async fn test_update_instance_config_vpc_prefix_network_update_multidpu( }; let initial_config = rpc::InstanceConfig { - tenant: Some(default_tenant_config()), + tenant: Some(fixture_tenant_config()), os: Some(initial_os.clone()), network: Some(network.clone()), infiniband: None, @@ -1483,7 +2211,7 @@ async fn test_update_instance_config_vpc_prefix_network_update_multidpu_differen }; let initial_config = rpc::InstanceConfig { - tenant: Some(default_tenant_config()), + tenant: Some(fixture_tenant_config()), os: Some(initial_os.clone()), network: Some(network.clone()), infiniband: None, @@ -1648,7 +2376,7 @@ async fn test_update_instance_config_vpc_prefix_network_update_different_prefix_ InstanceAllocationRequest::builder(false) .machine_id(mh.id) .config(rpc::InstanceConfig { - tenant: Some(default_tenant_config()), + tenant: Some(fixture_tenant_config()), os: Some(initial_os.clone()), network: Some(rpc::InstanceNetworkConfig { interfaces: vec![rpc::InstanceInterfaceConfig { @@ -1690,7 +2418,7 @@ async fn test_update_instance_config_vpc_prefix_network_update_different_prefix_ InstanceAllocationRequest::builder(false) .machine_id(mh.id) .config(rpc::InstanceConfig { - tenant: Some(default_tenant_config()), + tenant: Some(fixture_tenant_config()), os: Some(initial_os.clone()), network: Some(rpc::InstanceNetworkConfig { interfaces: vec![rpc::InstanceInterfaceConfig { @@ -1734,7 +2462,7 @@ async fn test_update_instance_config_vpc_prefix_network_update_different_prefix_ InstanceAllocationRequest::builder(false) .machine_id(mh.id) .config(rpc::InstanceConfig { - tenant: Some(default_tenant_config()), + tenant: Some(fixture_tenant_config()), os: Some(initial_os.clone()), network: Some(rpc::InstanceNetworkConfig { interfaces: vec![rpc::InstanceInterfaceConfig { @@ -1859,7 +2587,7 @@ async fn test_update_instance_config_vpc_prefix_network_update_different_prefix_ InstanceConfigUpdateRequest::builder() .instance_id(instance_id) .config(rpc::InstanceConfig { - tenant: Some(default_tenant_config()), + tenant: Some(fixture_tenant_config()), os: Some(initial_os.clone()), network: Some(rpc::InstanceNetworkConfig { interfaces: vec![ @@ -1881,7 +2609,7 @@ async fn test_update_instance_config_vpc_prefix_network_update_different_prefix_ device: Some("DPU1".to_string()), device_instance: 1, virtual_function_id: None, - ip_address: Some("6.6.6.6".to_string()), + ip_address: Some("6.6.6.7".to_string()), ipv6_interface_config: None, routing_profile: None, }, @@ -1919,7 +2647,7 @@ async fn test_update_instance_config_vpc_prefix_network_update_different_prefix_ InstanceConfigUpdateRequest::builder() .instance_id(instance_id) .config(rpc::InstanceConfig { - tenant: Some(default_tenant_config()), + tenant: Some(fixture_tenant_config()), os: Some(initial_os.clone()), network: Some(rpc::InstanceNetworkConfig { interfaces: vec![ @@ -1979,7 +2707,7 @@ async fn test_update_instance_config_vpc_prefix_network_update_different_prefix_ InstanceConfigUpdateRequest::builder() .instance_id(instance_id) .config(rpc::InstanceConfig { - tenant: Some(default_tenant_config()), + tenant: Some(fixture_tenant_config()), os: Some(initial_os.clone()), network: Some(rpc::InstanceNetworkConfig { interfaces: vec![ diff --git a/crates/api-core/src/tests/vpc_prefix.rs b/crates/api-core/src/tests/vpc_prefix.rs index 632baedd08..32b185d1e3 100644 --- a/crates/api-core/src/tests/vpc_prefix.rs +++ b/crates/api-core/src/tests/vpc_prefix.rs @@ -37,6 +37,14 @@ use crate::tests::common::api_fixtures::{ const REFERENCED_VPC_PREFIX: &str = "192.0.4.0/24"; const UNREFERENCED_VPC_PREFIX: &str = "192.1.4.0/27"; +/// Returns the tenant config that owns the shared VPC test fixture. +fn fixture_tenant_config() -> rpc::TenantConfig { + rpc::TenantConfig { + tenant_organization_id: FIXTURE_TENANT_ORG_ID.to_string(), + ..default_tenant_config() + } +} + #[derive(serde::Deserialize)] struct LifecycleStateJson { state: String, @@ -516,7 +524,7 @@ async fn test_deleted_vpc_prefix_cannot_allocate_new_instance_interface( machine_id: Some(managed_host.host().id), instance_type_id: None, config: Some(rpc::forge::InstanceConfig { - tenant: Some(default_tenant_config()), + tenant: Some(fixture_tenant_config()), os: Some(default_os_config()), network: Some(single_interface_network_config_with_vpc_prefix(id)), infiniband: None, @@ -565,6 +573,7 @@ async fn test_vpc_prefix_final_delete_after_generated_segment_cleanup( let managed_host = create_managed_host(&env).await; let (instance, rpc_instance) = managed_host .instance_builer(&env) + .tenant_org(FIXTURE_TENANT_ORG_ID) .network(single_interface_network_config_with_vpc_prefix(id)) .build_and_return() .await; diff --git a/crates/api-db/src/instance_address.rs b/crates/api-db/src/instance_address.rs index 92644db71c..e2eb105843 100644 --- a/crates/api-db/src/instance_address.rs +++ b/crates/api-db/src/instance_address.rs @@ -26,9 +26,7 @@ use ipnetwork::IpNetwork; use itertools::Itertools; use model::ConfigValidationError; use model::address_selection_strategy::AddressSelectionStrategy; -use model::instance::config::network::{ - InstanceInterfaceConfig, InstanceNetworkConfig, NetworkDetails, -}; +use model::instance::config::network::{InstanceInterfaceConfig, InstanceNetworkConfig}; use model::instance_address::InstanceAddress; use model::machine::Machine; use model::network_prefix::NetworkPrefix; @@ -316,13 +314,7 @@ pub async fn allocate( let segment_ids_using_vpc_prefix = updated_config .interfaces .iter() - .filter_map(|x| { - if let Some(NetworkDetails::VpcPrefixId(_)) = x.network_details { - x.network_segment_id - } else { - None - } - }) + .filter_map(InstanceInterfaceConfig::generated_network_segment_id) .collect_vec(); if segment_ids.len() != updated_config.interfaces.len() { @@ -809,6 +801,7 @@ mod tests { network_segment_id, ), ), + vpc_selection: None, ip_addrs: HashMap::default(), requested_ip_addr: None, ipv6_interface_config: None, diff --git a/crates/api-db/src/instance_network_config.rs b/crates/api-db/src/instance_network_config.rs index 2d0c9074c1..4e7513cbab 100644 --- a/crates/api-db/src/instance_network_config.rs +++ b/crates/api-db/src/instance_network_config.rs @@ -85,6 +85,7 @@ pub fn add_inband_interfaces_to_config( function_id: InterfaceFunctionId::Physical {}, network_segment_id: Some(*host_inband_segment_id), network_details: None, + vpc_selection: None, ip_addrs: Default::default(), interface_prefixes: Default::default(), network_segment_gateways: Default::default(), diff --git a/crates/api-db/src/vpc_prefix.rs b/crates/api-db/src/vpc_prefix.rs index 3df54fcc9f..f3234f64a6 100644 --- a/crates/api-db/src/vpc_prefix.rs +++ b/crates/api-db/src/vpc_prefix.rs @@ -128,30 +128,82 @@ where Ok(container) } -// Get a list of prefixes matching a filter on the ID column with ROW based lock. -pub async fn get_by_id_with_row_lock( +/// Loads explicit VPC-prefix selections for allocation validation. +/// +/// Deleted rows are deliberately included so the caller can distinguish an +/// unknown prefix from one that became unavailable through soft deletion. +/// This discovery query does not lock rows; allocation locks one candidate at +/// a time through [`lock_for_allocation`]. +pub async fn get_for_allocation_by_ids( txn: &mut PgConnection, - filter: &[VpcPrefixId], + vpc_prefix_ids: &[VpcPrefixId], ) -> Result, DatabaseError> { - let query = "SELECT * FROM network_vpc_prefixes WHERE id=ANY($1) FOR NO KEY UPDATE"; - let mut container: Vec = sqlx::query_as(query) - .bind(filter) + let query = r#" + SELECT * + FROM network_vpc_prefixes + WHERE id = ANY($1) + -- Include deleted rows so allocation validation can report them precisely. + ORDER BY id + "#; + sqlx::query_as(query) + .bind(vpc_prefix_ids) .fetch_all(&mut *txn) .await - .map_err(|e| DatabaseError::query(query, e))?; + .map_err(|e| DatabaseError::query(query, e)) +} - if let Some(vpc_prefix) = container - .iter() - .find(|prefix| prefix.is_marked_as_deleted()) - { - return Err(DatabaseError::InvalidArgument(format!( - "VPC prefix {} is marked for deletion and cannot be used for allocation", - vpc_prefix.id - ))); - } +/// Loads active automatic-allocation candidates for the requested VPCs. +/// +/// The `(vpc_id, id)` ordering is part of the allocator's cross-transaction +/// lock protocol. Callers must freeze this result for the outer transaction +/// rather than refreshing or re-ranking it using mutable capacity statistics. +pub async fn find_allocation_candidates( + txn: &mut PgConnection, + vpc_ids: &[VpcId], +) -> Result, DatabaseError> { + let query = r#" + SELECT * + FROM network_vpc_prefixes + WHERE vpc_id = ANY($1) + AND deleted IS NULL + -- Soft-deleted prefixes are not eligible automatic candidates. + AND ( + (family(prefix) = 4 AND masklen(prefix) < 31) + OR (family(prefix) = 6 AND masklen(prefix) < 127) + ) + -- A parent must be wider than the generated /31 or /127 linknet. + ORDER BY vpc_id, id + -- Stable ID order defines the allocator's prefix-row lock order. + "#; + sqlx::query_as(query) + .bind(vpc_ids) + .fetch_all(&mut *txn) + .await + .map_err(|e| DatabaseError::query(query, e)) +} - update_stats(&mut container, txn).await?; - Ok(container) +/// Locks and re-reads one allocation candidate. +/// +/// Returning `None` means the prefix was deleted after candidate discovery. +/// A successful savepoint retains this row lock in the containing transaction. +pub async fn lock_for_allocation( + txn: &mut PgConnection, + vpc_prefix_id: VpcPrefixId, +) -> Result, DatabaseError> { + let query = r#" + SELECT * + FROM network_vpc_prefixes + WHERE id = $1 + AND deleted IS NULL + -- Re-check deletion while acquiring the selected candidate lock. + FOR NO KEY UPDATE + -- Serialize allocation through this candidate's cursor. + "#; + sqlx::query_as(query) + .bind(vpc_prefix_id) + .fetch_optional(txn) + .await + .map_err(|e| DatabaseError::query(query, e)) } // Find the prefixes associated with a VPC. diff --git a/crates/api-integration-tests/tests/lib.rs b/crates/api-integration-tests/tests/lib.rs index 8755583942..8f64bad47f 100644 --- a/crates/api-integration-tests/tests/lib.rs +++ b/crates/api-integration-tests/tests/lib.rs @@ -224,6 +224,7 @@ async fn test_integration() -> eyre::Result<()> { HostHardwareType::DellPowerEdgeR750, &test_env, &bmc_address_registry, + tenant_org_id, &v4_vpc_prefix_id, &v6_vpc_prefix_id, // Relay IP in admin net @@ -1086,6 +1087,7 @@ async fn test_machine_a_tron_dual_stack( hw_type: HostHardwareType, test_env: &IntegrationTestEnvironment, bmc_mock_registry: &BmcMockRegistry, + tenant_organization_id: &str, v4_vpc_prefix_id: &str, v6_vpc_prefix_id: &str, admin_dhcp_relay_address: Ipv4Addr, @@ -1102,6 +1104,7 @@ async fn test_machine_a_tron_dual_stack( |machine_handle| { let v4_prefix_id = v4_vpc_prefix_id.to_string(); let v6_prefix_id = v6_vpc_prefix_id.to_string(); + let tenant_organization_id = tenant_organization_id.to_string(); let carbide_api_addrs = &test_env.carbide_api_addrs; async move { machine_handle @@ -1116,6 +1119,7 @@ async fn test_machine_a_tron_dual_stack( let instance_id = instance::create_with_vpc_prefixes( carbide_api_addrs, &machine_id, + &tenant_organization_id, &[&v4_prefix_id, &v6_prefix_id], ) .await?; diff --git a/crates/api-model/src/instance/config/network.rs b/crates/api-model/src/instance/config/network.rs index 182f221e73..754e11ead3 100644 --- a/crates/api-model/src/instance/config/network.rs +++ b/crates/api-model/src/instance/config/network.rs @@ -165,6 +165,7 @@ impl InstanceNetworkConfig { network_details: Some(NetworkDetails::NetworkSegment( network_segment_ids.first().copied().unwrap(), )), + vpc_selection: None, ip_addrs: HashMap::default(), requested_ip_addr: None, ipv6_interface_config: None, @@ -189,6 +190,7 @@ impl InstanceNetworkConfig { network_details: Some(NetworkDetails::NetworkSegment( network_segment_ids[dl_index], )), + vpc_selection: None, ip_addrs: HashMap::default(), requested_ip_addr: None, ipv6_interface_config: None, @@ -213,6 +215,7 @@ impl InstanceNetworkConfig { function_id: InterfaceFunctionId::Physical {}, network_segment_id: None, network_details: Some(NetworkDetails::VpcPrefixId(vpc_prefix_id)), + vpc_selection: None, ip_addrs: HashMap::default(), requested_ip_addr: None, ipv6_interface_config: None, @@ -423,15 +426,35 @@ impl InstanceNetworkConfig { iface.internal_uuid = uuid::Uuid::nil(); iface.vpc_id = None; + // Automatic intent is compared independently of its generated + // prefix, segment, and any legacy explicit-IP representation. + if iface.vpc_selection.is_some() { + iface.network_details = None; + iface.requested_ip_addr = None; + iface.ipv6_interface_config = None; + } + // It is possible that cloud sends network_segment_id with network_details as well. - if iface.network_details.is_some() { + if iface.network_details.is_some() || iface.vpc_selection.is_some() { iface.network_segment_id = None; } } for iface in &mut new_config.interfaces { + // A resolved automatic selection may be resubmitted from an + // internal caller; compare only its VPC and family intent. + if iface.vpc_selection.is_some() { + iface.network_details = None; + iface.requested_ip_addr = None; + iface.ipv6_interface_config = None; + iface.ip_addrs.clear(); + iface.interface_prefixes.clear(); + iface.network_segment_gateways.clear(); + iface.host_inband_mac_address = None; + } + // It is possible that cloud sends network_segment_id with network_details as well. - if iface.network_details.is_some() { + if iface.network_details.is_some() || iface.vpc_selection.is_some() { iface.network_segment_id = None; } iface.internal_uuid = uuid::Uuid::nil(); @@ -458,20 +481,54 @@ impl InstanceNetworkConfig { // config only with vf id as 0,1,3. for interface in &mut self.interfaces { let existing_interface = current_config.interfaces.iter().find(|x| { - let is_network_same = if interface.network_details.is_some() { - // TODO: && x.requested_ip_addr == interface.requested_ip_addr - // There's originally a gap here where it wasn't possible to change - // IPs without switching to a different prefix. It's technically - // possible to test requested_ip_addr so that explicit IP changes - // could trigger the update, even for the same VPC prefix, but it appears - // to trigger postgres table constraints. For now, the existing implementation - // gap is being maintained, and both will need to be resolved together. - x.network_details == interface.network_details - && x.ipv6_interface_config == interface.ipv6_interface_config - } else if interface.network_segment_id.is_some() { - x.network_segment_id == interface.network_segment_id - } else { - false + // An unresolved request may inherit the active resolution. Once + // both sides are resolved, prefix and segment identity must match + // so cleanup does not classify distinct resources as common. + let requested_resolution_matches = match interface.resolved_vpc_prefixes() { + None => true, + Some(requested_resolution) => { + x.resolved_vpc_prefixes() == Some(requested_resolution) + && x.generated_network_segment_id() + == interface.generated_network_segment_id() + } + }; + + let is_network_same = match (&interface.vpc_selection, &x.vpc_selection) { + (Some(requested), Some(existing)) => { + requested == existing && requested_resolution_matches + } + (Some(requested), None) => { + // Explicit-prefix intent may become automatic intent without + // replacing resources when its resolved VPC and families match. + x.vpc_id == Some(requested.vpc_id) + && x.resolved_vpc_prefixes().is_some_and(|resolved| { + match requested.family_mode { + InstanceInterfaceIpFamilyMode::Ipv4Only => { + resolved.ipv4_vpc_prefix_id.is_some() + && resolved.ipv6_vpc_prefix_id.is_none() + } + InstanceInterfaceIpFamilyMode::Ipv6Only => { + resolved.ipv4_vpc_prefix_id.is_none() + && resolved.ipv6_vpc_prefix_id.is_some() + } + InstanceInterfaceIpFamilyMode::DualStack => { + resolved.ipv4_vpc_prefix_id.is_some() + && resolved.ipv6_vpc_prefix_id.is_some() + } + } + }) + && requested_resolution_matches + } + _ if interface.network_details.is_some() => { + // TODO: Include requested IP intent after the existing + // address-update constraint behavior is resolved. + x.network_details == interface.network_details + && x.ipv6_interface_config == interface.ipv6_interface_config + } + _ => { + interface.network_segment_id.is_some() + && x.network_segment_id == interface.network_segment_id + } }; if is_network_same { @@ -484,14 +541,31 @@ impl InstanceNetworkConfig { }); if let Some(existing_interface) = existing_interface { - // Copy all allocated resources + // Copy all allocated resources. // TODO: Zero DPU changes. interface.ip_addrs = existing_interface.ip_addrs.clone(); - interface.requested_ip_addr = existing_interface.requested_ip_addr; - interface.ipv6_interface_config = existing_interface.ipv6_interface_config.clone(); interface.interface_prefixes = existing_interface.interface_prefixes.clone(); interface.network_segment_gateways = existing_interface.network_segment_gateways.clone(); + + if interface.vpc_selection.is_some() { + // Automatic intent reuses the resolution without reviving + // explicit address intent from an earlier configuration. + interface.network_details = existing_interface.network_details.clone(); + interface.requested_ip_addr = None; + interface.ipv6_interface_config = existing_interface + .ipv6_interface_config + .as_ref() + .map(|ipv6| Ipv6InterfaceConfig { + vpc_prefix_id: ipv6.vpc_prefix_id, + requested_ip_addr: None, + }); + } else { + interface.requested_ip_addr = existing_interface.requested_ip_addr; + interface.ipv6_interface_config = + existing_interface.ipv6_interface_config.clone(); + } + if interface.network_details.is_some() { interface.network_segment_id = existing_interface.network_segment_id; } @@ -590,6 +664,36 @@ pub fn validate_interface_function_ids< Ok(()) } +/// Address families requested for automatic VPC prefix and address selection. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum InstanceInterfaceIpFamilyMode { + /// Allocate one IPv4 prefix and interface address. + Ipv4Only, + /// Allocate one IPv6 prefix and interface address. + Ipv6Only, + /// Allocate one prefix and interface address from each family. + DualStack, +} + +/// Caller intent for automatic prefix and address selection from one VPC. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct InstanceInterfaceVpcSelection { + /// The single logical VPC from which prefixes must be selected. + pub vpc_id: VpcId, + /// The exact address families Core must allocate. + pub family_mode: InstanceInterfaceIpFamilyMode, +} + +/// VPC prefixes resolved for an instance interface, keyed by address family. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct InstanceInterfaceResolvedVpcPrefixes { + /// The selected IPv4 parent prefix, when IPv4 was requested. + pub ipv4_vpc_prefix_id: Option, + /// The selected IPv6 parent prefix, when IPv6 was requested. + pub ipv6_vpc_prefix_id: Option, +} + /// Enum to keep either network segment id or vpc_prefix id. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum NetworkDetails { @@ -632,6 +736,13 @@ pub struct InstanceInterfaceConfig { /// In case of vpc_prefix_id, carbide should allocate a new network segment and use it for /// further IP allocation. pub network_details: Option, + + /// Caller intent for automatic selection from a VPC. + /// + /// Resolved prefix IDs remain in the legacy-readable explicit fields. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub vpc_selection: Option, + /// The network segment this interface is attached to. /// In case vpc_prefix_id is provided, a new segment has to be created and assign here. pub network_segment_id: Option, @@ -705,6 +816,77 @@ pub struct InstanceInterfaceConfig { } impl InstanceInterfaceConfig { + /// Returns the resolved VPC prefix IDs keyed by address family. + pub fn resolved_vpc_prefixes(&self) -> Option { + let primary_vpc_prefix_id = match self.network_details.as_ref() { + Some(NetworkDetails::VpcPrefixId(vpc_prefix_id)) => *vpc_prefix_id, + _ => return None, + }; + + let ipv6_vpc_prefix_id = self + .ipv6_interface_config + .as_ref() + .map(|ipv6| ipv6.vpc_prefix_id); + + if let Some(selection) = self.vpc_selection { + return Some(match selection.family_mode { + InstanceInterfaceIpFamilyMode::Ipv4Only => InstanceInterfaceResolvedVpcPrefixes { + ipv4_vpc_prefix_id: Some(primary_vpc_prefix_id), + ipv6_vpc_prefix_id: None, + }, + InstanceInterfaceIpFamilyMode::Ipv6Only => InstanceInterfaceResolvedVpcPrefixes { + ipv4_vpc_prefix_id: None, + ipv6_vpc_prefix_id: Some(primary_vpc_prefix_id), + }, + InstanceInterfaceIpFamilyMode::DualStack => InstanceInterfaceResolvedVpcPrefixes { + ipv4_vpc_prefix_id: Some(primary_vpc_prefix_id), + ipv6_vpc_prefix_id, + }, + }); + } + + if ipv6_vpc_prefix_id.is_some() { + return Some(InstanceInterfaceResolvedVpcPrefixes { + ipv4_vpc_prefix_id: Some(primary_vpc_prefix_id), + ipv6_vpc_prefix_id, + }); + } + + let primary_is_ipv6 = self + .requested_ip_addr + .is_some_and(|address| address.is_ipv6()) + || self.ip_addrs.values().any(|address| address.is_ipv6()) + || self + .interface_prefixes + .values() + .any(|prefix| prefix.is_ipv6()) + || self + .network_segment_gateways + .values() + .any(|gateway| gateway.is_ipv6()); + + Some(if primary_is_ipv6 { + InstanceInterfaceResolvedVpcPrefixes { + ipv4_vpc_prefix_id: None, + ipv6_vpc_prefix_id: Some(primary_vpc_prefix_id), + } + } else { + // Existing family-agnostic records predate IPv6 allocation and + // therefore use IPv4 when no persisted family evidence exists. + InstanceInterfaceResolvedVpcPrefixes { + ipv4_vpc_prefix_id: Some(primary_vpc_prefix_id), + ipv6_vpc_prefix_id: None, + } + }) + } + + /// Returns the generated segment associated with resolved explicit or + /// automatic VPC-prefix intent. + pub fn generated_network_segment_id(&self) -> Option { + self.resolved_vpc_prefixes()?; + self.network_segment_id + } + /// Returns true if this instance interface is equivalent to the host's in-band interface, /// meaning it belong to a network segment of type [`NetworkSegmentType::HostInband`]. This is /// in contrast to DPU-based interfaces where the instance sees an overlay network. @@ -779,7 +961,7 @@ where #[cfg(test)] mod tests { use carbide_test_support::Outcome::*; - use carbide_test_support::scenarios; + use carbide_test_support::{scenarios, value_scenarios}; use super::*; @@ -852,6 +1034,7 @@ mod tests { network_segment_gateways, host_inband_mac_address: None, network_details: None, + vpc_selection: None, device_locator: None, internal_uuid, vpc_id: None, @@ -891,6 +1074,7 @@ mod tests { network_segment_gateways: HashMap::default(), host_inband_mac_address: None, network_details: None, + vpc_selection: None, device_locator: None, internal_uuid: uuid::Uuid::new_v4(), vpc_id: None, @@ -904,6 +1088,381 @@ mod tests { } } + /// Builds one resolved automatic interface while retaining the usual + /// service-generated representation for its selected prefixes. + fn resolved_vpc_interface( + selection: InstanceInterfaceVpcSelection, + primary_vpc_prefix_id: VpcPrefixId, + ipv6_vpc_prefix_id: Option, + ) -> InstanceInterfaceConfig { + let mut interface = create_valid_network_config().interfaces.swap_remove(0); + interface.network_details = Some(NetworkDetails::VpcPrefixId(primary_vpc_prefix_id)); + interface.vpc_selection = Some(selection); + interface.ipv6_interface_config = + ipv6_vpc_prefix_id.map(|vpc_prefix_id| Ipv6InterfaceConfig { + vpc_prefix_id, + requested_ip_addr: None, + }); + interface.vpc_id = Some(selection.vpc_id); + interface + } + + /// Resolved prefixes are projected by family rather than by the storage + /// position used for rolling compatibility. + #[test] + fn resolved_vpc_prefixes_follow_family_mode() { + let vpc_id = VpcId::new(); + let ipv4_vpc_prefix_id = VpcPrefixId::new(); + let ipv6_vpc_prefix_id = VpcPrefixId::new(); + + value_scenarios!( + run = |(family_mode, primary_vpc_prefix_id, secondary_vpc_prefix_id)| { + resolved_vpc_interface( + InstanceInterfaceVpcSelection { + vpc_id, + family_mode, + }, + primary_vpc_prefix_id, + secondary_vpc_prefix_id, + ) + .resolved_vpc_prefixes() + }; + "IPv4 only" { + ( + InstanceInterfaceIpFamilyMode::Ipv4Only, + ipv4_vpc_prefix_id, + None, + ) => Some(InstanceInterfaceResolvedVpcPrefixes { + ipv4_vpc_prefix_id: Some(ipv4_vpc_prefix_id), + ipv6_vpc_prefix_id: None, + }), + } + "IPv6 only" { + ( + InstanceInterfaceIpFamilyMode::Ipv6Only, + ipv6_vpc_prefix_id, + None, + ) => Some(InstanceInterfaceResolvedVpcPrefixes { + ipv4_vpc_prefix_id: None, + ipv6_vpc_prefix_id: Some(ipv6_vpc_prefix_id), + }), + } + "dual stack" { + ( + InstanceInterfaceIpFamilyMode::DualStack, + ipv4_vpc_prefix_id, + Some(ipv6_vpc_prefix_id), + ) => Some(InstanceInterfaceResolvedVpcPrefixes { + ipv4_vpc_prefix_id: Some(ipv4_vpc_prefix_id), + ipv6_vpc_prefix_id: Some(ipv6_vpc_prefix_id), + }), + } + ); + } + + /// Legacy explicit-prefix storage infers the primary family from its + /// address data and treats an IPv6 sidecar as explicit dual stack. + #[test] + fn resolved_vpc_prefixes_support_explicit_prefix_storage() { + let vpc_id = VpcId::new(); + let ipv4_vpc_prefix_id = VpcPrefixId::new(); + let ipv6_vpc_prefix_id = VpcPrefixId::new(); + + let mut ipv4 = resolved_vpc_interface( + InstanceInterfaceVpcSelection { + vpc_id, + family_mode: InstanceInterfaceIpFamilyMode::Ipv4Only, + }, + ipv4_vpc_prefix_id, + None, + ); + ipv4.vpc_selection = None; + + let mut ipv6 = resolved_vpc_interface( + InstanceInterfaceVpcSelection { + vpc_id, + family_mode: InstanceInterfaceIpFamilyMode::Ipv6Only, + }, + ipv6_vpc_prefix_id, + None, + ); + ipv6.vpc_selection = None; + ipv6.requested_ip_addr = Some("2001:db8::10".parse().unwrap()); + + let mut dual_stack = resolved_vpc_interface( + InstanceInterfaceVpcSelection { + vpc_id, + family_mode: InstanceInterfaceIpFamilyMode::DualStack, + }, + ipv4_vpc_prefix_id, + Some(ipv6_vpc_prefix_id), + ); + dual_stack.vpc_selection = None; + + value_scenarios!( + run = |interface| interface.resolved_vpc_prefixes(); + "IPv4 only" { + ipv4 => Some(InstanceInterfaceResolvedVpcPrefixes { + ipv4_vpc_prefix_id: Some(ipv4_vpc_prefix_id), + ipv6_vpc_prefix_id: None, + }), + } + "IPv6 only" { + ipv6 => Some(InstanceInterfaceResolvedVpcPrefixes { + ipv4_vpc_prefix_id: None, + ipv6_vpc_prefix_id: Some(ipv6_vpc_prefix_id), + }), + } + "dual stack" { + dual_stack => Some(InstanceInterfaceResolvedVpcPrefixes { + ipv4_vpc_prefix_id: Some(ipv4_vpc_prefix_id), + ipv6_vpc_prefix_id: Some(ipv6_vpc_prefix_id), + }), + } + ); + } + + /// Automatic selection metadata round-trips additively while the explicit + /// resolution remains readable if an older representation ignores it. + #[test] + fn serialize_resolved_vpc_selection_additively() { + let selection = InstanceInterfaceVpcSelection { + vpc_id: VpcId::new(), + family_mode: InstanceInterfaceIpFamilyMode::DualStack, + }; + let interface = + resolved_vpc_interface(selection, VpcPrefixId::new(), Some(VpcPrefixId::new())); + + let mut serialized = serde_json::to_value(&interface).unwrap(); + assert!(serialized.get("vpc_selection").is_some()); + assert_eq!( + serde_json::from_value::(serialized.clone()).unwrap(), + interface + ); + + // Dropping the additive field models a rolling peer that understands + // only the retained explicit-prefix storage representation. + serialized.as_object_mut().unwrap().remove("vpc_selection"); + let legacy_view = serde_json::from_value::(serialized).unwrap(); + assert_eq!(legacy_view.vpc_selection, None); + assert_eq!(legacy_view.network_details, interface.network_details); + assert_eq!( + legacy_view.ipv6_interface_config, + interface.ipv6_interface_config + ); + } + + /// Update comparison ignores generated automatic resolution, but still + /// detects every caller-controlled selection change. + #[test] + fn network_update_detection_compares_vpc_selection_intent() { + let selection = InstanceInterfaceVpcSelection { + vpc_id: VpcId::new(), + family_mode: InstanceInterfaceIpFamilyMode::Ipv4Only, + }; + let mut current = create_valid_network_config(); + current.interfaces.truncate(1); + current.interfaces[0] = resolved_vpc_interface(selection, VpcPrefixId::new(), None); + + let mut unresolved = current.clone(); + unresolved.interfaces[0].network_details = None; + unresolved.interfaces[0].network_segment_id = None; + + let mut alternate_resolution = current.clone(); + alternate_resolution.interfaces[0].network_details = + Some(NetworkDetails::VpcPrefixId(VpcPrefixId::new())); + alternate_resolution.interfaces[0].network_segment_id = Some(offset_segment_id(42)); + + let mut changed_family = unresolved.clone(); + changed_family.interfaces[0].vpc_selection = Some(InstanceInterfaceVpcSelection { + family_mode: InstanceInterfaceIpFamilyMode::DualStack, + ..selection + }); + + let mut changed_vpc = unresolved.clone(); + changed_vpc.interfaces[0].vpc_selection = Some(InstanceInterfaceVpcSelection { + vpc_id: VpcId::new(), + ..selection + }); + + value_scenarios!( + run = |requested| current.is_network_config_update_requested(&requested); + "unresolved repetition" { + unresolved => false, + } + "same intent with alternate generated resolution" { + alternate_resolution => false, + } + "changed family mode" { + changed_family => true, + } + "changed VPC" { + changed_vpc => true, + } + ); + } + + /// An unresolved repetition inherits the active automatic resolution and + /// is returned as a common resource for cleanup filtering. + #[test] + fn copy_existing_resources_resolves_matching_vpc_selection() { + let selection = InstanceInterfaceVpcSelection { + vpc_id: VpcId::new(), + family_mode: InstanceInterfaceIpFamilyMode::Ipv4Only, + }; + let mut current = create_valid_network_config(); + current.interfaces.truncate(1); + current.interfaces[0] = resolved_vpc_interface(selection, VpcPrefixId::new(), None); + let network_prefix_id = NetworkPrefixId::new(); + current.interfaces[0] + .ip_addrs + .insert(network_prefix_id, "192.0.2.10".parse().unwrap()); + current.interfaces[0] + .interface_prefixes + .insert(network_prefix_id, "192.0.2.10/32".parse().unwrap()); + current.interfaces[0] + .network_segment_gateways + .insert(network_prefix_id, "192.0.2.1/24".parse().unwrap()); + + let expected_resolution = current.interfaces[0].resolved_vpc_prefixes(); + let expected_segment_id = current.interfaces[0].network_segment_id; + let expected_ip_addrs = current.interfaces[0].ip_addrs.clone(); + let mut requested = current.clone(); + requested.interfaces[0].network_details = None; + requested.interfaces[0].network_segment_id = None; + requested.interfaces[0].vpc_id = None; + requested.interfaces[0].ip_addrs.clear(); + requested.interfaces[0].interface_prefixes.clear(); + requested.interfaces[0].network_segment_gateways.clear(); + + let common = requested.copy_existing_resources(¤t); + + assert_eq!(common.len(), 1); + assert_eq!( + requested.interfaces[0].resolved_vpc_prefixes(), + expected_resolution + ); + assert_eq!( + requested.interfaces[0].network_segment_id, + expected_segment_id + ); + assert_eq!(requested.interfaces[0].ip_addrs, expected_ip_addrs); + } + + /// Switching active explicit prefixes to matching automatic intent reuses + /// their generated resources without retaining old explicit IP requests. + #[test] + fn copy_existing_resources_reuses_explicit_prefix_for_vpc_selection() { + let vpc_id = VpcId::new(); + let ipv4_vpc_prefix_id = VpcPrefixId::new(); + let ipv6_vpc_prefix_id = VpcPrefixId::new(); + let selection = InstanceInterfaceVpcSelection { + vpc_id, + family_mode: InstanceInterfaceIpFamilyMode::DualStack, + }; + let mut current = create_valid_network_config(); + current.interfaces.truncate(1); + current.interfaces[0].network_details = + Some(NetworkDetails::VpcPrefixId(ipv4_vpc_prefix_id)); + current.interfaces[0].vpc_id = Some(vpc_id); + current.interfaces[0].requested_ip_addr = Some("192.0.2.10".parse().unwrap()); + current.interfaces[0].ipv6_interface_config = Some(Ipv6InterfaceConfig { + vpc_prefix_id: ipv6_vpc_prefix_id, + requested_ip_addr: Some("2001:db8::10".parse().unwrap()), + }); + let ipv4_network_prefix_id = NetworkPrefixId::new(); + let ipv6_network_prefix_id = NetworkPrefixId::new(); + current.interfaces[0] + .ip_addrs + .insert(ipv4_network_prefix_id, "192.0.2.10".parse().unwrap()); + current.interfaces[0] + .ip_addrs + .insert(ipv6_network_prefix_id, "2001:db8::10".parse().unwrap()); + current.interfaces[0] + .interface_prefixes + .insert(ipv4_network_prefix_id, "192.0.2.10/32".parse().unwrap()); + current.interfaces[0] + .interface_prefixes + .insert(ipv6_network_prefix_id, "2001:db8::10/128".parse().unwrap()); + current.interfaces[0] + .network_segment_gateways + .insert(ipv4_network_prefix_id, "192.0.2.1/24".parse().unwrap()); + current.interfaces[0] + .network_segment_gateways + .insert(ipv6_network_prefix_id, "2001:db8::1/64".parse().unwrap()); + let expected_segment_id = current.interfaces[0].network_segment_id; + let expected_ip_addrs = current.interfaces[0].ip_addrs.clone(); + + let mut requested = create_valid_network_config(); + requested.interfaces.truncate(1); + requested.interfaces[0].vpc_selection = Some(selection); + requested.interfaces[0].network_segment_id = None; + + assert!(current.is_network_config_update_requested(&requested)); + let common = requested.copy_existing_resources(¤t); + + assert_eq!(common.len(), 1); + assert_eq!( + requested.interfaces[0].resolved_vpc_prefixes(), + Some(InstanceInterfaceResolvedVpcPrefixes { + ipv4_vpc_prefix_id: Some(ipv4_vpc_prefix_id), + ipv6_vpc_prefix_id: Some(ipv6_vpc_prefix_id), + }) + ); + assert_eq!( + requested.interfaces[0].network_segment_id, + expected_segment_id + ); + assert_eq!(requested.interfaces[0].ip_addrs, expected_ip_addrs); + assert_eq!(requested.interfaces[0].requested_ip_addr, None); + assert_eq!( + requested.interfaces[0] + .ipv6_interface_config + .as_ref() + .and_then(|ipv6| ipv6.requested_ip_addr), + None + ); + assert_eq!(requested.interfaces[0].vpc_selection, Some(selection)); + } + + /// Requests already resolved to a different prefix or segment must not + /// reuse or protect the active resources during cleanup. + #[test] + fn copy_existing_resources_preserves_alternate_resolution() { + let selection = InstanceInterfaceVpcSelection { + vpc_id: VpcId::new(), + family_mode: InstanceInterfaceIpFamilyMode::Ipv4Only, + }; + let mut current = create_valid_network_config(); + current.interfaces.truncate(1); + current.interfaces[0] = resolved_vpc_interface(selection, VpcPrefixId::new(), None); + + let mut alternate_prefix = create_valid_network_config(); + alternate_prefix.interfaces.truncate(1); + alternate_prefix.interfaces[0] = + resolved_vpc_interface(selection, VpcPrefixId::new(), None); + + let mut alternate_segment = current.clone(); + alternate_segment.interfaces[0].network_segment_id = Some(offset_segment_id(42)); + + value_scenarios!( + run = |mut requested| { + let expected_resolution = requested.interfaces[0].resolved_vpc_prefixes(); + let expected_segment_id = requested.interfaces[0].network_segment_id; + let common = requested.copy_existing_resources(¤t); + common.is_empty() + && requested.interfaces[0].resolved_vpc_prefixes() == expected_resolution + && requested.interfaces[0].network_segment_id == expected_segment_id + }; + "different selected prefix" { + alternate_prefix => true, + } + "different generated segment" { + alternate_segment => true, + } + ); + } + #[test] fn network_update_detection_ignores_derived_vpc_id() { let mut current = create_valid_network_config(); @@ -1005,6 +1564,7 @@ mod tests { function_id: InterfaceFunctionId::Physical {}, network_segment_id: Some(offset_segment_id(idx)), network_details: None, + vpc_selection: None, ip_addrs: HashMap::default(), requested_ip_addr: None, ipv6_interface_config: None, diff --git a/crates/api-model/src/instance/status/network.rs b/crates/api-model/src/instance/status/network.rs index 945667d887..2ffe7ef891 100644 --- a/crates/api-model/src/instance/status/network.rs +++ b/crates/api-model/src/instance/status/network.rs @@ -30,7 +30,8 @@ use serde::{Deserialize, Serialize}; use crate::SerializableMacAddress; use crate::instance::config::network::{ - InstanceInterfaceConfig, InstanceNetworkConfig, InterfaceFunctionId, + InstanceInterfaceConfig, InstanceInterfaceResolvedVpcPrefixes, InstanceNetworkConfig, + InterfaceFunctionId, }; use crate::instance::status::SyncState; use crate::machine::Machine; @@ -149,6 +150,7 @@ impl InstanceNetworkStatus { prefixes: obs_iface.prefixes.clone(), gateways: obs_iface.gateways.clone(), vpc_id: config_iface.vpc_id, + resolved_vpc_prefixes: config_iface.resolved_vpc_prefixes(), device: config_iface .device_locator .as_ref() @@ -177,6 +179,7 @@ impl InstanceNetworkStatus { prefixes: Vec::new(), gateways: Vec::new(), vpc_id: config_iface.vpc_id, + resolved_vpc_prefixes: config_iface.resolved_vpc_prefixes(), device: config_iface .device_locator .as_ref() @@ -199,6 +202,7 @@ impl InstanceNetworkStatus { prefixes: Vec::new(), gateways: Vec::new(), vpc_id: config_iface.vpc_id, + resolved_vpc_prefixes: config_iface.resolved_vpc_prefixes(), device: config_iface .device_locator .as_ref() @@ -246,6 +250,7 @@ impl InstanceNetworkStatus { prefixes: intf_obs.prefixes.clone(), gateways: intf_obs.gateways.clone(), vpc_id: config_iface.vpc_id, + resolved_vpc_prefixes: config_iface.resolved_vpc_prefixes(), device: config_iface .device_locator .as_ref() @@ -273,6 +278,7 @@ impl InstanceNetworkStatus { prefixes: Vec::new(), gateways: Vec::new(), vpc_id: config_iface.vpc_id, + resolved_vpc_prefixes: config_iface.resolved_vpc_prefixes(), device: config_iface .device_locator .as_ref() @@ -320,6 +326,7 @@ impl InstanceNetworkStatus { prefixes: Vec::new(), gateways: Vec::new(), vpc_id: iface.vpc_id, + resolved_vpc_prefixes: iface.resolved_vpc_prefixes(), device: iface.device_locator.as_ref().map(|dl| dl.device.clone()), device_instance: iface .device_locator @@ -377,6 +384,9 @@ pub struct InstanceInterfaceStatus { /// The logical VPC this interface belongs to. pub vpc_id: Option, + /// VPC prefixes resolved for this interface, keyed by address family. + pub resolved_vpc_prefixes: Option, + pub device: Option, pub device_instance: usize, } @@ -386,6 +396,7 @@ impl InstanceInterfaceStatus { /// Host-inband interfaces do not get real network status observations, so we construct status /// ourselves from the host interface's config. pub fn from_host_inband_interface(mut value: InstanceInterfaceConfig) -> Self { + let resolved_vpc_prefixes = value.resolved_vpc_prefixes(); let (prefix_ids, addresses): (Vec<_>, Vec<_>) = value.ip_addrs.into_iter().unzip(); // For each NetworkPrefixId we saw in ip_addrs, get that entry from the @@ -419,6 +430,7 @@ impl InstanceInterfaceStatus { prefixes, gateways, vpc_id: value.vpc_id, + resolved_vpc_prefixes, device: None, device_instance: 0, } @@ -527,9 +539,13 @@ mod tests { use std::str::FromStr; use carbide_uuid::network::{NetworkPrefixId, NetworkSegmentId}; + use carbide_uuid::vpc::VpcPrefixId; use super::*; - use crate::instance::config::network::InstanceInterfaceConfig; + use crate::instance::config::network::{ + InstanceInterfaceConfig, InstanceInterfaceIpFamilyMode, InstanceInterfaceVpcSelection, + Ipv6InterfaceConfig, NetworkDetails, + }; use crate::network_security_group::NetworkSecurityGroupSource; #[test] @@ -660,6 +676,7 @@ mod tests { )]), host_inband_mac_address: None, network_details: None, + vpc_selection: None, device_locator: None, internal_uuid: uuid::Uuid::new_v4(), vpc_id: None, @@ -684,6 +701,7 @@ mod tests { )]), host_inband_mac_address: None, network_details: None, + vpc_selection: None, device_locator: None, internal_uuid: uuid::Uuid::new_v4(), vpc_id: None, @@ -708,6 +726,7 @@ mod tests { )]), host_inband_mac_address: None, network_details: None, + vpc_selection: None, device_locator: None, internal_uuid: uuid::Uuid::new_v4(), vpc_id: None, @@ -745,6 +764,7 @@ mod tests { )]), host_inband_mac_address: Some(MacAddress::new([1, 2, 3, 4, 5, 6])), network_details: None, + vpc_selection: None, device_locator: None, internal_uuid: internal_uuid1, vpc_id: None, @@ -769,6 +789,7 @@ mod tests { )]), host_inband_mac_address: Some(MacAddress::new([1, 2, 3, 4, 5, 16])), network_details: None, + vpc_selection: None, device_locator: None, internal_uuid: internal_uuid2, vpc_id: None, @@ -793,6 +814,7 @@ mod tests { )]), host_inband_mac_address: Some(MacAddress::new([1, 2, 3, 4, 5, 26])), network_details: None, + vpc_selection: None, device_locator: None, internal_uuid: internal_uuid3, vpc_id: None, @@ -860,6 +882,7 @@ mod tests { prefixes: Vec::new(), gateways: Vec::new(), vpc_id: None, + resolved_vpc_prefixes: None, device: None, device_instance: 0, }, @@ -870,6 +893,7 @@ mod tests { prefixes: Vec::new(), gateways: Vec::new(), vpc_id: None, + resolved_vpc_prefixes: None, device: None, device_instance: 0, }, @@ -880,6 +904,7 @@ mod tests { prefixes: Vec::new(), gateways: Vec::new(), vpc_id: None, + resolved_vpc_prefixes: None, device: None, device_instance: 0, }, @@ -901,6 +926,7 @@ mod tests { prefixes: iface.interface_prefixes.values().copied().collect(), gateways: iface.network_segment_gateways.values().copied().collect(), vpc_id: iface.vpc_id, + resolved_vpc_prefixes: iface.resolved_vpc_prefixes(), device: iface.device_locator.as_ref().map(|dl| dl.device.clone()), device_instance: iface .device_locator @@ -917,6 +943,7 @@ mod tests { prefixes: iface.interface_prefixes.values().copied().collect(), gateways: iface.network_segment_gateways.values().copied().collect(), vpc_id: iface.vpc_id, + resolved_vpc_prefixes: iface.resolved_vpc_prefixes(), device: iface.device_locator.as_ref().map(|dl| dl.device.clone()), device_instance: iface .device_locator @@ -934,6 +961,7 @@ mod tests { prefixes: iface.interface_prefixes.values().copied().collect(), gateways: iface.network_segment_gateways.values().copied().collect(), vpc_id: iface.vpc_id, + resolved_vpc_prefixes: iface.resolved_vpc_prefixes(), device: iface.device_locator.as_ref().map(|dl| dl.device.clone()), device_instance: iface .device_locator @@ -958,6 +986,7 @@ mod tests { prefixes: vec!["127.0.1.0/24".parse().unwrap()], gateways: vec!["127.0.1.1/24".parse().unwrap()], vpc_id: None, + resolved_vpc_prefixes: None, device: None, device_instance: 0, }, @@ -968,6 +997,7 @@ mod tests { prefixes: vec!["127.0.2.0/24".parse().unwrap()], gateways: vec!["127.0.2.1/24".parse().unwrap()], vpc_id: None, + resolved_vpc_prefixes: None, device: None, device_instance: 0, }, @@ -978,6 +1008,7 @@ mod tests { prefixes: vec!["127.0.3.0/24".parse().unwrap()], gateways: vec!["127.0.3.1/24".parse().unwrap()], vpc_id: None, + resolved_vpc_prefixes: None, device: None, device_instance: 0, }, @@ -1000,6 +1031,43 @@ mod tests { assert_eq!(status, unsynced_status()) } + /// Allocation-derived prefix resolution remains visible while observed + /// interface addresses are still pending synchronization. + #[test] + fn network_status_without_observations_includes_resolved_prefixes() { + let vpc_id = VpcId::new(); + let ipv4_vpc_prefix_id = VpcPrefixId::new(); + let ipv6_vpc_prefix_id = VpcPrefixId::new(); + let mut config = network_config(); + let interface = &mut config.interfaces[0]; + interface.network_details = Some(NetworkDetails::VpcPrefixId(ipv4_vpc_prefix_id)); + interface.vpc_selection = Some(InstanceInterfaceVpcSelection { + vpc_id, + family_mode: InstanceInterfaceIpFamilyMode::DualStack, + }); + interface.ipv6_interface_config = Some(Ipv6InterfaceConfig { + vpc_prefix_id: ipv6_vpc_prefix_id, + requested_ip_addr: None, + }); + interface.vpc_id = Some(vpc_id); + + let status = InstanceNetworkStatus::from_config_and_observations( + HashMap::default(), + Versioned::new(&config, ConfigVersion::initial()), + &HashMap::default(), + false, + ); + + assert!(status.interfaces[0].addresses.is_empty()); + assert_eq!( + status.interfaces[0].resolved_vpc_prefixes, + Some(InstanceInterfaceResolvedVpcPrefixes { + ipv4_vpc_prefix_id: Some(ipv4_vpc_prefix_id), + ipv6_vpc_prefix_id: Some(ipv6_vpc_prefix_id), + }) + ); + } + #[test] fn network_status_with_correct_version_observation() { let config = network_config(); diff --git a/crates/api-test-helper/src/instance.rs b/crates/api-test-helper/src/instance.rs index fa29649cea..4ada7ed1f8 100644 --- a/crates/api-test-helper/src/instance.rs +++ b/crates/api-test-helper/src/instance.rs @@ -124,6 +124,7 @@ pub async fn create( pub async fn create_with_vpc_prefixes( addrs: &[SocketAddr], host_machine_id: &MachineId, + tenant_organization_id: &str, vpc_prefix_ids: &[&str], ) -> eyre::Result { tracing::info!( @@ -149,7 +150,7 @@ pub async fn create_with_vpc_prefixes( "machine_id": {"id": host_machine_id}, "config": { "tenant": { - "tenant_organization_id": "MyOrg", + "tenant_organization_id": tenant_organization_id, }, "network": { "interfaces": [iface] diff --git a/crates/machine-controller/src/handler.rs b/crates/machine-controller/src/handler.rs index 3f01c4a6ce..2133ecc474 100644 --- a/crates/machine-controller/src/handler.rs +++ b/crates/machine-controller/src/handler.rs @@ -59,7 +59,7 @@ use model::dpa_interface::DpaInterfaceControllerState; use model::firmware::{Firmware, FirmwareComponentType, FirmwareEntry}; use model::instance::InstanceNetworkSyncStatus; use model::instance::config::network::{ - DeviceLocator, InstanceInterfaceConfig, InterfaceFunctionId, NetworkDetails, + DeviceLocator, InstanceInterfaceConfig, InterfaceFunctionId, }; use model::instance::snapshot::InstanceSnapshot; use model::instance::status::SyncState; @@ -6439,13 +6439,10 @@ impl StateHandler for InstanceStateHandler { .network .interfaces .iter() - .filter_map(|x| match x.network_details { - Some(NetworkDetails::VpcPrefixId(_)) => x.network_segment_id, - _ => None, - }) + .filter_map(InstanceInterfaceConfig::generated_network_segment_id) .collect_vec(); - // No network segment is configured with vpc_prefix_id. + // No generated VPC-prefix segment needs readiness tracking. if network_segment_ids_with_vpc.is_empty() { return Ok(StateHandlerOutcome::transition(next_state)); } @@ -7571,13 +7568,10 @@ async fn handle_instance_network_config_update_request( .new_config .interfaces .iter() - .filter_map(|x| match x.network_details { - Some(NetworkDetails::VpcPrefixId(_)) => x.network_segment_id, - _ => None, - }) + .filter_map(InstanceInterfaceConfig::generated_network_segment_id) .collect_vec(); - // No network segment is configured with vpc_prefix_id. + // Generated VPC-prefix segments must be ready before promotion. if !network_segment_ids_with_vpc.is_empty() { let network_segments_are_ready = db::network_segment::are_network_segments_ready( &mut ctx.services.db_reader, @@ -7934,13 +7928,10 @@ async fn release_network_segments_with_vpc_prefix( ) -> Result<(), StateHandlerError> { let network_segment_ids_with_vpc = interfaces .iter() - .filter_map(|x| match x.network_details { - Some(NetworkDetails::VpcPrefixId(_)) => x.network_segment_id, - _ => None, - }) + .filter_map(InstanceInterfaceConfig::generated_network_segment_id) .collect_vec(); - // Mark all network ready for delete which were created for vpc_prefixes. + // Mark generated VPC-prefix segments ready for deletion. if !network_segment_ids_with_vpc.is_empty() { db::network_segment::mark_as_deleted_no_validation(txn, &network_segment_ids_with_vpc) .await diff --git a/crates/rpc/build.rs b/crates/rpc/build.rs index 76ec5963df..1159273263 100644 --- a/crates/rpc/build.rs +++ b/crates/rpc/build.rs @@ -138,6 +138,10 @@ fn main() -> Result<(), Box> { "forge.InstanceInterfaceConfig.network_details", "#[derive(serde::Serialize)]", ) + .type_attribute( + "forge.InstanceInterfaceVpcSelection", + "#[derive(serde::Serialize)]", + ) .type_attribute( "forge.InstanceIBInterfaceConfig", "#[derive(serde::Deserialize, serde::Serialize)]", @@ -229,6 +233,10 @@ fn main() -> Result<(), Box> { "forge.InstanceInterfaceStatus", "#[derive(serde::Serialize)]", ) + .type_attribute( + "forge.InstanceInterfaceResolvedVpcPrefixes", + "#[derive(serde::Serialize)]", + ) .type_attribute( "forge.InstanceIBInterfaceStatus", "#[derive(serde::Serialize)]", diff --git a/crates/rpc/proto/forge.proto b/crates/rpc/proto/forge.proto index ee83b9e32d..cb2178d3cf 100644 --- a/crates/rpc/proto/forge.proto +++ b/crates/rpc/proto/forge.proto @@ -129,11 +129,9 @@ service Forge { // rpc UpdateInstanceNetworkConfig(InstanceNetworkConfigUpdateRequest) returns (Instance); // Updates the Operating System on a running instance rpc UpdateInstanceOperatingSystem(InstanceOperatingSystemUpdateRequest) returns (Instance); - // Updates the configuration of a running instance. - // Only the following configurations of an instance can be changed: - // - Keyset IDs - // - Metadata (Name, Description, Labels) - // - Operating System Details + // Updates the mutable desired configuration and metadata of a running instance. + // `config` and `metadata` are complete values, not patches. Callers must + // preserve fields and selector variants they do not intend to change. // The update will take effect asynchronously. Users should monitor `Instance.status.synced` // to determine whether all updates have been applied. rpc UpdateInstanceConfig(InstanceConfigUpdateRequest) returns (Instance); @@ -1796,10 +1794,10 @@ message VpcUpdateVirtualizationRequest { // a `ConcurrentModificationError`. optional string if_version_match = 2; - // network_virtualization_type is the new VpcVirtualizationType - // to set for the VPC. This is intended for dev use only, and - // will return an error if any instances are already configured - // in the VPC. + // Development or maintenance operation. The only supported use migrates a + // legacy ETHERNET_VIRTUALIZER VPC with no instances to FNN. Quiesce instance + // allocation into that VPC before starting and until completion. Do not + // transition an existing FNN or FLAT VPC, or transition any VPC to FLAT. optional VpcVirtualizationType network_virtualization_type = 3; } @@ -3024,7 +3022,9 @@ message InstanceConfigUpdateRequest { // a `ConcurrentModificationError`. optional string if_version_match = 2; - // New configuration to be utilized by the instance + // Complete desired configuration replacing the current configuration. The + // server does not merge omitted fields. Read-modify-write clients must + // preserve fields and oneof variants they do not intend to change. InstanceConfig config = 3; // New Metadata to be utilized by the instance Metadata metadata = 4; @@ -3239,6 +3239,11 @@ message InstanceInterfaceConfig { // and continue IP allocation. This field is address-family agnostic — // it can be an IPv4 or IPv6 VPC prefix. common.VpcPrefixId vpc_prefix_id = 6; + // Select prefixes and addresses automatically from this FNN VPC. + // This selector records caller intent. Clients replacing `InstanceConfig` + // must preserve it unless intentionally switching to an explicit segment + // or prefix. + InstanceInterfaceVpcSelection vpc = 13; } // The ID of security group that is attached to this interface @@ -3279,6 +3284,29 @@ message InstanceInterfaceConfig { optional InstanceInterfaceRoutingProfile routing_profile = 12; } +// Automatic VPC-backed network selection for one instance interface. +message InstanceInterfaceVpcSelection { + // The single logical VPC this interface belongs to. + common.VpcId vpc_id = 1; + // The address families Core must allocate. Unspecified is invalid. + InstanceInterfaceIpFamilyMode family_mode = 2; +} + +// Address families requested for automatic VPC prefix and address selection. +// +// Core models and persists every mode. External IPv6-only and dual-stack +// requests are temporarily rejected until downstream DPU support is complete. +enum InstanceInterfaceIpFamilyMode { + // Invalid for an automatic VPC selection request. + INSTANCE_INTERFACE_IP_FAMILY_MODE_UNSPECIFIED = 0; + // Allocate one IPv4 prefix and address. + INSTANCE_INTERFACE_IP_FAMILY_MODE_IPV4_ONLY = 1; + // Allocate one IPv6 prefix and address. + INSTANCE_INTERFACE_IP_FAMILY_MODE_IPV6_ONLY = 2; + // Allocate one prefix and address from each family in the same VPC. + INSTANCE_INTERFACE_IP_FAMILY_MODE_DUAL_STACK = 3; +} + // IPv6 configuration for a dual-stack instance interface. message InstanceInterfaceIpv6Config { // The IPv6 VPC prefix to allocate from. The primary vpc_prefix_id creates @@ -3329,6 +3357,12 @@ message InstanceIBInterfaceConfig { common.IBPartitionId ib_partition_id = 21; } +// VPC prefixes Core resolved for an instance interface, keyed by family. +message InstanceInterfaceResolvedVpcPrefixes { + optional common.VpcPrefixId ipv4_vpc_prefix_id = 1; + optional common.VpcPrefixId ipv6_vpc_prefix_id = 2; +} + // The actual status of a single network interface of an instance message InstanceInterfaceStatus { // If the interface is defined as a virtual function (associated @@ -3363,6 +3397,9 @@ message InstanceInterfaceStatus { // The logical VPC this interface belongs to. optional common.VpcId vpc_id = 8; + + // Prefixes resolved from explicit-prefix or automatic VPC intent. + optional InstanceInterfaceResolvedVpcPrefixes resolved_vpc_prefixes = 9; } // The actual status of a single IB interface of an instance diff --git a/crates/rpc/src/model/instance/config/network.rs b/crates/rpc/src/model/instance/config/network.rs index 77b1c44656..4411c2aa84 100644 --- a/crates/rpc/src/model/instance/config/network.rs +++ b/crates/rpc/src/model/instance/config/network.rs @@ -20,9 +20,10 @@ use std::net::IpAddr; use itertools::Itertools; use model::instance::config::network::{ - DeviceLocator, InstanceInterfaceConfig, InstanceInterfaceRoutingProfile, - InstanceNetworkAutoConfig, InstanceNetworkConfig, InterfaceFunctionId, InterfaceFunctionType, - Ipv6InterfaceConfig, NetworkDetails, + DeviceLocator, InstanceInterfaceConfig, InstanceInterfaceIpFamilyMode, + InstanceInterfaceRoutingProfile, InstanceInterfaceVpcSelection, InstanceNetworkAutoConfig, + InstanceNetworkConfig, InterfaceFunctionId, InterfaceFunctionType, Ipv6InterfaceConfig, + NetworkDetails, }; use crate as rpc; @@ -49,6 +50,68 @@ impl From for rpc::InterfaceFunctionType { } } +/// Converts a recognized wire family mode into the complete internal model. +impl TryFrom for InstanceInterfaceIpFamilyMode { + type Error = RpcDataConversionError; + + fn try_from(value: forge::InstanceInterfaceIpFamilyMode) -> Result { + match value { + forge::InstanceInterfaceIpFamilyMode::Unspecified => { + Err(RpcDataConversionError::InvalidArgument( + "InstanceInterfaceVpcSelection::family_mode must be specified".to_string(), + )) + } + forge::InstanceInterfaceIpFamilyMode::Ipv4Only => Ok(Self::Ipv4Only), + forge::InstanceInterfaceIpFamilyMode::Ipv6Only => Ok(Self::Ipv6Only), + forge::InstanceInterfaceIpFamilyMode::DualStack => Ok(Self::DualStack), + } + } +} + +/// Converts every internal family mode for wire config output. +impl From for forge::InstanceInterfaceIpFamilyMode { + fn from(value: InstanceInterfaceIpFamilyMode) -> Self { + match value { + InstanceInterfaceIpFamilyMode::Ipv4Only => Self::Ipv4Only, + InstanceInterfaceIpFamilyMode::Ipv6Only => Self::Ipv6Only, + InstanceInterfaceIpFamilyMode::DualStack => Self::DualStack, + } + } +} + +/// Validates required fields in an automatic VPC selector. +impl TryFrom for InstanceInterfaceVpcSelection { + type Error = RpcDataConversionError; + + fn try_from(value: forge::InstanceInterfaceVpcSelection) -> Result { + let wire_family_mode = forge::InstanceInterfaceIpFamilyMode::try_from(value.family_mode) + .map_err(|_| { + RpcDataConversionError::InvalidArgument(format!( + "unknown InstanceInterfaceVpcSelection::family_mode: {}", + value.family_mode + )) + })?; + let family_mode = InstanceInterfaceIpFamilyMode::try_from(wire_family_mode)?; + + Ok(Self { + vpc_id: value.vpc_id.ok_or(RpcDataConversionError::MissingArgument( + "InstanceInterfaceVpcSelection::vpc_id", + ))?, + family_mode, + }) + } +} + +/// Reconstructs caller-owned automatic VPC intent for wire output. +impl From for forge::InstanceInterfaceVpcSelection { + fn from(value: InstanceInterfaceVpcSelection) -> Self { + Self { + vpc_id: Some(value.vpc_id), + family_mode: forge::InstanceInterfaceIpFamilyMode::from(value.family_mode) as i32, + } + } +} + #[derive(PartialEq)] enum VFAllocationType { // Only physical interface is defined. No virtual function is defined. @@ -185,32 +248,61 @@ impl TryFrom for InstanceNetworkConfig { } }; - // If network_details is present, that gets precedence and we'll pull the network_segment_id from that - // if it's a NetworkSegment. - let (network_details, network_segment_id) = if let Some(x) = iface.network_details { - let nd: NetworkDetails = x.try_into()?; - let ns_id = match nd { - NetworkDetails::NetworkSegment(network_segment_id) => Some(network_segment_id), - NetworkDetails::VpcPrefixId(_uuid) => None, - }; - - (Some(nd), ns_id) - } else { - // If network_details wasn't set, then the caller is required to - // send network_segment_id. - // This is old model. Let's use network segment id as such. - // TODO: This should be removed in future. - let ns_id = - iface - .network_segment_id - .ok_or(RpcDataConversionError::MissingArgument( - "InstanceInterfaceConfig::network_segment_id", - ))?; - - // And then we'll populate network_details from that as well. - (Some(NetworkDetails::NetworkSegment(ns_id)), Some(ns_id)) + // The protobuf oneof makes segment, explicit-prefix, and automatic + // VPC selection mutually exclusive for all generated clients. + let (network_details, vpc_selection, network_segment_id) = match iface.network_details { + Some(rpc::forge::instance_interface_config::NetworkDetails::SegmentId( + network_segment_id, + )) => ( + Some(NetworkDetails::NetworkSegment(network_segment_id)), + None, + Some(network_segment_id), + ), + Some(rpc::forge::instance_interface_config::NetworkDetails::VpcPrefixId( + vpc_prefix_id, + )) => (Some(NetworkDetails::VpcPrefixId(vpc_prefix_id)), None, None), + Some(rpc::forge::instance_interface_config::NetworkDetails::Vpc(selection)) => ( + None, + Some(InstanceInterfaceVpcSelection::try_from(selection)?), + None, + ), + None => { + // Legacy callers may still populate only the standalone + // segment field; canonicalize that into the selector. + let network_segment_id = + iface + .network_segment_id + .ok_or(RpcDataConversionError::MissingArgument( + "InstanceInterfaceConfig::network_segment_id", + ))?; + ( + Some(NetworkDetails::NetworkSegment(network_segment_id)), + None, + Some(network_segment_id), + ) + } }; + if vpc_selection.is_some() + && (iface.ip_address.is_some() || iface.ipv6_interface_config.is_some()) + { + return Err(RpcDataConversionError::InvalidArgument( + "automatic VPC selection cannot be combined with explicit IP configuration" + .to_string(), + )); + } + + // Core models and allocation support every family. + // TODO: Accept automatic IPv6 modes once downstream DPU support is + // complete end to end. + if let Some(selection) = vpc_selection + && selection.family_mode != InstanceInterfaceIpFamilyMode::Ipv4Only + { + return Err(RpcDataConversionError::InvalidArgument( + "automatic VPC selection currently supports only IPV4_ONLY".to_string(), + )); + } + if iface.ip_address.is_some() && matches!(network_details, Some(NetworkDetails::NetworkSegment(..))) { @@ -295,6 +387,7 @@ impl TryFrom for InstanceNetworkConfig { function_id, network_segment_id, network_details, + vpc_selection, ip_addrs: HashMap::default(), requested_ip_addr: iface .ip_address @@ -343,11 +436,32 @@ impl TryFrom for rpc::InstanceNetworkConfig { for iface in config.interfaces.into_iter() { let function_type = iface.function_id.function_type(); - // Update network segment id based on network details. - let network_details: Option = - iface.network_details.map(|x| x.into()); + // Caller-owned automatic intent replaces the internal explicit + // resolution when projecting config back onto the wire. + let network_details = match iface.vpc_selection { + Some(selection) => Some( + rpc::forge::instance_interface_config::NetworkDetails::Vpc(selection.into()), + ), + None => iface.network_details.map(Into::into), + }; let network_segment_id = iface.network_segment_id; + // Automatic mode owns address selection, so never leak persisted + // resolution details back as explicit caller requests. + let (ip_address, ipv6_interface_config) = if iface.vpc_selection.is_some() { + (None, None) + } else { + ( + iface.requested_ip_addr.map(|ip| ip.to_string()), + iface.ipv6_interface_config.map(|ipv6| { + rpc::forge::InstanceInterfaceIpv6Config { + vpc_prefix_id: Some(ipv6.vpc_prefix_id), + ip_address: ipv6.requested_ip_addr.map(|ip| ip.to_string()), + } + }), + ) + }; + let (device, device_instance) = match iface.device_locator { Some(dl) => (Some(dl.device), dl.device_instance as u32), None => (None, 0), @@ -365,13 +479,8 @@ impl TryFrom for rpc::InstanceNetworkConfig { device, device_instance, virtual_function_id, - ip_address: iface.requested_ip_addr.map(|i| i.to_string()), - ipv6_interface_config: iface.ipv6_interface_config.map(|v6| { - rpc::forge::InstanceInterfaceIpv6Config { - vpc_prefix_id: Some(v6.vpc_prefix_id), - ip_address: v6.requested_ip_addr.map(|i| i.to_string()), - } - }), + ip_address, + ipv6_interface_config, routing_profile: iface.routing_profile.map(|profile| { rpc::forge::InstanceInterfaceRoutingProfile { allowed_anycast_prefixes: profile @@ -443,6 +552,11 @@ impl TryFrom for NetworkD rpc::forge::instance_interface_config::NetworkDetails::VpcPrefixId(vpc_prefix_id) => { NetworkDetails::VpcPrefixId(vpc_prefix_id) } + rpc::forge::instance_interface_config::NetworkDetails::Vpc(_) => { + return Err(RpcDataConversionError::InvalidArgument( + "automatic VPC selection is not an explicit NetworkDetails value".to_string(), + )); + } }) } } @@ -464,6 +578,222 @@ mod tests { uuid::Uuid::from_u128(BASE_SEGMENT_ID.as_u128() + offset as u128).into() } + /// Builds one wire interface whose network selector is automatic VPC + /// intent, allowing invalid raw enum values to exercise boundary checks. + fn rpc_vpc_interface(vpc_id: Option, family_mode: i32) -> rpc::InstanceInterfaceConfig { + rpc::InstanceInterfaceConfig { + function_type: rpc::InterfaceFunctionType::Physical as i32, + network_segment_id: None, + network_details: Some(rpc::forge::instance_interface_config::NetworkDetails::Vpc( + forge::InstanceInterfaceVpcSelection { + vpc_id, + family_mode, + }, + )), + device: None, + device_instance: 0, + virtual_function_id: None, + ip_address: None, + ipv6_interface_config: None, + routing_profile: None, + } + } + + /// Converts one-interface wire config and reports boundary + /// acceptance without coupling cases to a particular error string. + fn accepts_rpc_interface(interface: rpc::InstanceInterfaceConfig) -> bool { + InstanceNetworkConfig::try_from(rpc::InstanceNetworkConfig { + interfaces: vec![interface], + #[allow(deprecated)] + auto: false, + auto_config: None, + }) + .is_ok() + } + + /// Typed family conversion models future IPv6 modes even while the + /// external allocation boundary temporarily accepts only IPv4. + #[test] + fn convert_vpc_selection_family_modes() { + value_scenarios!( + run = |family_mode| InstanceInterfaceIpFamilyMode::try_from(family_mode).ok(); + "IPv4 only" { + forge::InstanceInterfaceIpFamilyMode::Ipv4Only => Some(InstanceInterfaceIpFamilyMode::Ipv4Only), + } + "IPv6 only" { + forge::InstanceInterfaceIpFamilyMode::Ipv6Only => Some(InstanceInterfaceIpFamilyMode::Ipv6Only), + } + "dual stack" { + forge::InstanceInterfaceIpFamilyMode::DualStack => Some(InstanceInterfaceIpFamilyMode::DualStack), + } + "unspecified" { + forge::InstanceInterfaceIpFamilyMode::Unspecified => None, + } + ); + } + + /// The inbound RPC boundary rejects unspecified, unknown, missing, and + /// not-yet-supported family requests while accepting IPv4 automatic mode. + #[test] + fn validate_inbound_vpc_selection_modes() { + let vpc_id = VpcId::new(); + + value_scenarios!( + run = |(vpc_id, family_mode)| accepts_rpc_interface(rpc_vpc_interface(vpc_id, family_mode)); + "IPv4 only" { + (Some(vpc_id), forge::InstanceInterfaceIpFamilyMode::Ipv4Only as i32) => true, + } + "IPv6 only is not yet supported" { + (Some(vpc_id), forge::InstanceInterfaceIpFamilyMode::Ipv6Only as i32) => false, + } + "dual stack is not yet supported" { + (Some(vpc_id), forge::InstanceInterfaceIpFamilyMode::DualStack as i32) => false, + } + "unspecified" { + (Some(vpc_id), forge::InstanceInterfaceIpFamilyMode::Unspecified as i32) => false, + } + "unknown raw value" { + (Some(vpc_id), i32::MAX) => false, + } + "missing VPC" { + (None, forge::InstanceInterfaceIpFamilyMode::Ipv4Only as i32) => false, + } + ); + } + + /// Automatic VPC selection owns all address choices and therefore rejects + /// both primary and IPv6 explicit address configuration. + #[test] + fn reject_explicit_addresses_with_vpc_selection() { + let vpc_id = VpcId::new(); + let mut primary_ip = rpc_vpc_interface( + Some(vpc_id), + forge::InstanceInterfaceIpFamilyMode::Ipv4Only as i32, + ); + primary_ip.ip_address = Some("192.0.2.10".to_string()); + + let mut ipv6_config = rpc_vpc_interface( + Some(vpc_id), + forge::InstanceInterfaceIpFamilyMode::Ipv4Only as i32, + ); + ipv6_config.ipv6_interface_config = Some(forge::InstanceInterfaceIpv6Config { + vpc_prefix_id: Some(VpcPrefixId::new()), + ip_address: None, + }); + + value_scenarios!( + run = accepts_rpc_interface; + "primary IP" { + primary_ip => false, + } + "IPv6 configuration" { + ipv6_config => false, + } + ); + } + + /// Outbound config preserves every typed family intent and hides the + /// rolling-compatible explicit prefix and address representation. + #[test] + fn outbound_vpc_selection_suppresses_internal_resolution() { + let vpc_id = VpcId::new(); + let ipv4_vpc_prefix_id = VpcPrefixId::new(); + let ipv6_vpc_prefix_id = VpcPrefixId::new(); + + value_scenarios!( + run = |family_mode| { + let (primary_vpc_prefix_id, requested_ip_addr, ipv6_interface_config) = + match family_mode { + InstanceInterfaceIpFamilyMode::Ipv4Only => ( + ipv4_vpc_prefix_id, + Some("192.0.2.10".parse().unwrap()), + None, + ), + InstanceInterfaceIpFamilyMode::Ipv6Only => ( + ipv6_vpc_prefix_id, + Some("2001:db8::10".parse().unwrap()), + None, + ), + InstanceInterfaceIpFamilyMode::DualStack => ( + ipv4_vpc_prefix_id, + Some("192.0.2.10".parse().unwrap()), + Some(Ipv6InterfaceConfig { + vpc_prefix_id: ipv6_vpc_prefix_id, + requested_ip_addr: Some("2001:db8::10".parse().unwrap()), + }), + ), + }; + let config = InstanceNetworkConfig { + interfaces: vec![InstanceInterfaceConfig { + function_id: InterfaceFunctionId::Physical {}, + network_segment_id: Some(offset_segment_id(0)), + network_details: Some(NetworkDetails::VpcPrefixId( + primary_vpc_prefix_id, + )), + vpc_selection: Some(InstanceInterfaceVpcSelection { + vpc_id, + family_mode, + }), + ip_addrs: HashMap::default(), + requested_ip_addr, + ipv6_interface_config, + routing_profile: None, + interface_prefixes: HashMap::default(), + network_segment_gateways: HashMap::default(), + host_inband_mac_address: None, + device_locator: None, + internal_uuid: uuid::Uuid::new_v4(), + vpc_id: Some(vpc_id), + }], + auto_config: None, + }; + + let wire: rpc::InstanceNetworkConfig = config.try_into().unwrap(); + let interface = wire.interfaces.into_iter().next().unwrap(); + let selection = match interface.network_details.unwrap() { + rpc::forge::instance_interface_config::NetworkDetails::Vpc(selection) => { + selection + } + other => panic!("expected VPC selection, got {other:?}"), + }; + ( + selection.vpc_id, + selection.family_mode, + interface.ip_address, + interface.ipv6_interface_config, + interface.network_segment_id, + ) + }; + "IPv4 only" { + InstanceInterfaceIpFamilyMode::Ipv4Only => ( + Some(vpc_id), + forge::InstanceInterfaceIpFamilyMode::Ipv4Only as i32, + None, + None, + Some(offset_segment_id(0)), + ), + } + "IPv6 only" { + InstanceInterfaceIpFamilyMode::Ipv6Only => ( + Some(vpc_id), + forge::InstanceInterfaceIpFamilyMode::Ipv6Only as i32, + None, + None, + Some(offset_segment_id(0)), + ), + } + "dual stack" { + InstanceInterfaceIpFamilyMode::DualStack => ( + Some(vpc_id), + forge::InstanceInterfaceIpFamilyMode::DualStack as i32, + None, + None, + Some(offset_segment_id(0)), + ), + } + ); + } + #[test] fn assign_ids_from_rpc_config_pf_only() { let config = rpc::InstanceNetworkConfig { @@ -497,6 +827,7 @@ mod tests { network_segment_gateways: HashMap::new(), host_inband_mac_address: None, network_details: Some(NetworkDetails::NetworkSegment(BASE_SEGMENT_ID.into()),), + vpc_selection: None, device_locator: None, internal_uuid: netconfig.interfaces.first().unwrap().internal_uuid, vpc_id: None, @@ -551,6 +882,7 @@ mod tests { network_segment_gateways: HashMap::new(), host_inband_mac_address: None, network_details: Some(NetworkDetails::NetworkSegment(BASE_SEGMENT_ID.into())), + vpc_selection: None, device_locator: None, internal_uuid: netconf_interfaces_iter.next().unwrap().internal_uuid, vpc_id: None, @@ -569,6 +901,7 @@ mod tests { network_segment_gateways: HashMap::new(), host_inband_mac_address: None, network_details: Some(NetworkDetails::NetworkSegment(segment_id)), + vpc_selection: None, device_locator: None, internal_uuid: netconf_interfaces_iter.next().unwrap().internal_uuid, vpc_id: None, @@ -746,6 +1079,7 @@ mod tests { function_id: InterfaceFunctionId::Physical {}, network_segment_id: None, network_details: Some(NetworkDetails::VpcPrefixId(v4_id)), + vpc_selection: None, ip_addrs: HashMap::default(), requested_ip_addr: None, ipv6_interface_config: Some(Ipv6InterfaceConfig { diff --git a/crates/rpc/src/model/instance/status/network.rs b/crates/rpc/src/model/instance/status/network.rs index 22cdef0920..e08880e4fa 100644 --- a/crates/rpc/src/model/instance/status/network.rs +++ b/crates/rpc/src/model/instance/status/network.rs @@ -72,6 +72,12 @@ impl TryFrom for rpc::InstanceInterfaceStatus { device: status.device, device_instance: status.device_instance as u32, vpc_id: status.vpc_id, + resolved_vpc_prefixes: status.resolved_vpc_prefixes.map(|resolved| { + rpc::forge::InstanceInterfaceResolvedVpcPrefixes { + ipv4_vpc_prefix_id: resolved.ipv4_vpc_prefix_id, + ipv6_vpc_prefix_id: resolved.ipv6_vpc_prefix_id, + } + }), }) } } @@ -144,3 +150,41 @@ impl TryFrom for InstanceInterfaceStatu }) } } + +#[cfg(test)] +mod tests { + use carbide_uuid::vpc::{VpcId, VpcPrefixId}; + use model::instance::config::network::InstanceInterfaceResolvedVpcPrefixes; + + use super::*; + + /// Status conversion keeps both family-keyed prefix IDs for a resolved + /// dual-stack interface in its single logical VPC. + #[test] + fn convert_dual_stack_resolved_vpc_prefixes() { + let vpc_id = VpcId::new(); + let ipv4_vpc_prefix_id = VpcPrefixId::new(); + let ipv6_vpc_prefix_id = VpcPrefixId::new(); + let status = InstanceInterfaceStatus { + function_id: InterfaceFunctionId::Physical {}, + mac_address: None, + addresses: Vec::new(), + prefixes: Vec::new(), + gateways: Vec::new(), + vpc_id: Some(vpc_id), + resolved_vpc_prefixes: Some(InstanceInterfaceResolvedVpcPrefixes { + ipv4_vpc_prefix_id: Some(ipv4_vpc_prefix_id), + ipv6_vpc_prefix_id: Some(ipv6_vpc_prefix_id), + }), + device: None, + device_instance: 0, + }; + + let wire = rpc::InstanceInterfaceStatus::try_from(status).unwrap(); + let resolved = wire.resolved_vpc_prefixes.unwrap(); + + assert_eq!(wire.vpc_id, Some(vpc_id)); + assert_eq!(resolved.ipv4_vpc_prefix_id, Some(ipv4_vpc_prefix_id)); + assert_eq!(resolved.ipv6_vpc_prefix_id, Some(ipv6_vpc_prefix_id)); + } +} diff --git a/rest-api/docs/index.html b/rest-api/docs/index.html index ea1ec69c3f..1b4187d4fb 100644 --- a/rest-api/docs/index.html +++ b/rest-api/docs/index.html @@ -464,7 +464,7 @@ -
Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
{
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "kind": "SiteWideRoot",
  • "password": "string",
  • "username": "string",
  • "macAddress": "string"
}

Response samples

Content type
application/json
{
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "kind": "SiteWideRoot",
  • "username": "string",
  • "macAddress": "string"
}

Allocation

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/credential/bmc

Request samples

Content type
application/json
{
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "kind": "SiteWideRoot",
  • "password": "string",
  • "username": "string",
  • "macAddress": "string"
}

Response samples

Content type
application/json
{
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "kind": "SiteWideRoot",
  • "username": "string",
  • "macAddress": "string"
}

UEFI Credential

UEFI Credential endpoints allow creating site-default host and DPU UEFI credentials for a Site

+

Create UEFI Credential

Create a site-default host or DPU UEFI credential. The request fails +if the selected site-default credential already exists.

+

User must have authorization role with PROVIDER_ADMIN suffix.

+
Authorizations:
JWTBearerToken
path Parameters
org
required
string

Name of the Org

+
Request Body schema: application/json
required
siteId
required
string <uuid>

ID of the Site where the credential is stored.

+
kind
required
string
Enum: "Host" "DPU"

Which site-default UEFI credential to create.

+
password
required
string non-empty

Credential password.

+

Responses

Response Schema: application/json
siteId
required
string <uuid>

ID of the Site where the credential is stored.

+
kind
required
string
Enum: "Host" "DPU"

Which site-default UEFI credential was created.

+

Request samples

Content type
application/json
{
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "kind": "Host",
  • "password": "string"
}

Response samples

Content type
application/json
{
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "kind": "Host"
}

Allocation

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
[
  • {
    }
]

Create Allocation

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/allocation

Response samples

Content type
application/json
[
  • {
    }
]

Create Allocation

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
{
  • "name": "Echo Studios",
  • "description": "Echo Studios resource allocation in SJC4",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "allocationConstraints": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "Echo Studios",
  • "description": "Echo Studios resource allocation in SJC4",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z",
  • "allocationConstraints": [
    ]
}

Retrieve Allocation

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/allocation

Request samples

Content type
application/json
{
  • "name": "Echo Studios",
  • "description": "Echo Studios resource allocation in SJC4",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "allocationConstraints": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "Echo Studios",
  • "description": "Echo Studios resource allocation in SJC4",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z",
  • "allocationConstraints": [
    ]
}

Retrieve Allocation

Retrieve Allocation by ID

@@ -2274,7 +2304,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "Echo Studios",
  • "description": "Echo Studios resource allocation in SJC4",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z",
  • "allocationConstraints": [
    ]
}

Delete Allocation

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/allocation/{allocationId}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "Echo Studios",
  • "description": "Echo Studios resource allocation in SJC4",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z",
  • "allocationConstraints": [
    ]
}

Delete Allocation

Delete an Allocation by ID.

@@ -2290,7 +2320,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

Update Allocation

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/allocation/{allocationId}

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

Update Allocation

Update an existing Allocation

@@ -2366,7 +2396,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
{
  • "name": "Echo Studios Compute",
  • "description": "Echo Studios compute resource allocation in SJC4"
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "Echo Studios Compute",
  • "description": "Echo Studios compute resource allocation in SJC4",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z",
  • "allocationConstraints": [
    ]
}

Update Allocation Constraint

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/allocation/{allocationId}

Request samples

Content type
application/json
{
  • "name": "Echo Studios Compute",
  • "description": "Echo Studios compute resource allocation in SJC4"
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "Echo Studios Compute",
  • "description": "Echo Studios compute resource allocation in SJC4",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z",
  • "allocationConstraints": [
    ]
}

Update Allocation Constraint

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
{
  • "constraintValue": 20
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "allocationId": "9ec871ce-3363-4de2-8f79-062881067628",
  • "resourceType": "InstanceType",
  • "resourceTypeId": "a59ee688-b5e5-4606-9891-f4a605edacd3",
  • "constraintType": "Reserved",
  • "constraintValue": 20,
  • "derivedResourceId": null,
  • "instanceType": {
    },
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

VPC

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/allocation/{allocationId}/constraint/{allocationConstraintId}

Request samples

Content type
application/json
{
  • "constraintValue": 20
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "allocationId": "9ec871ce-3363-4de2-8f79-062881067628",
  • "resourceType": "InstanceType",
  • "resourceTypeId": "a59ee688-b5e5-4606-9891-f4a605edacd3",
  • "constraintType": "Reserved",
  • "constraintValue": 20,
  • "derivedResourceId": null,
  • "instanceType": {
    },
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

VPC

VPC defines the networking isolation boundary for Tenant's Instances.

Retrieve all VPCs

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
[
  • {
    }
]

Create VPC

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/vpc

Response samples

Content type
application/json
[
  • {
    }
]

Create VPC

Create a VPC for the org.

Org must have a Tenant entity. User must have authorization role with TENANT_ADMIN suffix

@@ -2650,7 +2680,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "spark-vpc",
  • "description": "Virtual network for machines executing Spark jobs",
  • "siteId": "72771e6a-6f5e-4de4-a5b9-1266c4197811",
  • "networkVirtualizationType": "ETHERNET_VIRTUALIZER",
  • "vni": 12001,
  • "nvLinkLogicalPartitionId": "dd887330-dbd3-45ce-b400-c42fc8e47315",
  • "labels": {
    }
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "spark-vpc",
  • "description": "Virtual network for machines executing Spark jobs",
  • "org": "xskkpgqpeakn",
  • "tenantId": "34f5c98e-f430-457b-a812-92637d0c6fd0",
  • "siteId": "72771e6a-6f5e-4de4-a5b9-1266c4197811",
  • "controllerVpcId": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "networkVirtualizationType": "ETHERNET_VIRTUALIZER",
  • "requestedVni": 12001,
  • "vni": 12001,
  • "nvLinkLogicalPartitionId": "dd887330-dbd3-45ce-b400-c42fc8e47315",
  • "labels": {
    },
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve a VPC

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/vpc

Request samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "spark-vpc",
  • "description": "Virtual network for machines executing Spark jobs",
  • "siteId": "72771e6a-6f5e-4de4-a5b9-1266c4197811",
  • "networkVirtualizationType": "ETHERNET_VIRTUALIZER",
  • "vni": 12001,
  • "nvLinkLogicalPartitionId": "dd887330-dbd3-45ce-b400-c42fc8e47315",
  • "labels": {
    }
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "spark-vpc",
  • "description": "Virtual network for machines executing Spark jobs",
  • "org": "xskkpgqpeakn",
  • "tenantId": "34f5c98e-f430-457b-a812-92637d0c6fd0",
  • "siteId": "72771e6a-6f5e-4de4-a5b9-1266c4197811",
  • "controllerVpcId": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "networkVirtualizationType": "ETHERNET_VIRTUALIZER",
  • "requestedVni": 12001,
  • "vni": 12001,
  • "nvLinkLogicalPartitionId": "dd887330-dbd3-45ce-b400-c42fc8e47315",
  • "labels": {
    },
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve a VPC

Retrieve a specific VPC by ID.

Org must have a Tenant entity. User must have authorization role with TENANT_ADMIN suffix.

@@ -2738,7 +2768,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "spark-vpc",
  • "description": "Virtual network for machines executing Spark jobs",
  • "org": "xskkpgqpeakn",
  • "tenantId": "34f5c98e-f430-457b-a812-92637d0c6fd0",
  • "siteId": "72771e6a-6f5e-4de4-a5b9-1266c4197811",
  • "controllerVpcId": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "networkVirtualizationType": "ETHERNET_VIRTUALIZER",
  • "requestedVni": 12001,
  • "vni": 12001,
  • "nvLinkLogicalPartitionId": "dd887330-dbd3-45ce-b400-c42fc8e47315",
  • "labels": {
    },
  • "status": "Ready",
  • "networkSecurityGroupId": "c602eb90-3039-11f0-997a-b38d4fc8389e",
  • "networkSecurityGroupPropagationDetails": {
    },
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete a VPC

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/vpc/{vpcId}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "spark-vpc",
  • "description": "Virtual network for machines executing Spark jobs",
  • "org": "xskkpgqpeakn",
  • "tenantId": "34f5c98e-f430-457b-a812-92637d0c6fd0",
  • "siteId": "72771e6a-6f5e-4de4-a5b9-1266c4197811",
  • "controllerVpcId": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "networkVirtualizationType": "ETHERNET_VIRTUALIZER",
  • "requestedVni": 12001,
  • "vni": 12001,
  • "nvLinkLogicalPartitionId": "dd887330-dbd3-45ce-b400-c42fc8e47315",
  • "labels": {
    },
  • "status": "Ready",
  • "networkSecurityGroupId": "c602eb90-3039-11f0-997a-b38d4fc8389e",
  • "networkSecurityGroupPropagationDetails": {
    },
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete a VPC

Delete a specific VPC by ID.

Org must have a Tenant entity. User must have authorization role with TENANT_ADMIN suffix.

@@ -2752,7 +2782,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

Update VPC

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/vpc/{vpcId}

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

Update VPC

Update an existing VPC

Org must have a Tenant entity. User must have authorization role with TENANT_ADMIN suffix

@@ -2850,7 +2880,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
{
  • "name": "spark-vpc-v1",
  • "description": "Virtual network for machines executing Spark jobs v1",
  • "labels": {
    }
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "spark-vpc-v1",
  • "description": "Virtual network for machines executing Spark jobs v1",
  • "org": "xskkpgqpeakn",
  • "tenantId": "34f5c98e-f430-457b-a812-92637d0c6fd0",
  • "siteId": "72771e6a-6f5e-4de4-a5b9-1266c4197811",
  • "controllerVpcId": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "networkVirtualizationType": "ETHERNET_VIRTUALIZER",
  • "requestedVni": 12001,
  • "vni": 12001,
  • "nvLinkLogicalPartitionId": "dd887330-dbd3-45ce-b400-c42fc8e47315",
  • "labels": {
    },
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Update VPC Virtualization

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/vpc/{vpcId}

Request samples

Content type
application/json
{
  • "name": "spark-vpc-v1",
  • "description": "Virtual network for machines executing Spark jobs v1",
  • "labels": {
    }
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "spark-vpc-v1",
  • "description": "Virtual network for machines executing Spark jobs v1",
  • "org": "xskkpgqpeakn",
  • "tenantId": "34f5c98e-f430-457b-a812-92637d0c6fd0",
  • "siteId": "72771e6a-6f5e-4de4-a5b9-1266c4197811",
  • "controllerVpcId": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "networkVirtualizationType": "ETHERNET_VIRTUALIZER",
  • "requestedVni": 12001,
  • "vni": 12001,
  • "nvLinkLogicalPartitionId": "dd887330-dbd3-45ce-b400-c42fc8e47315",
  • "labels": {
    },
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Update VPC Virtualization

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
{
  • "networkVirtualizationType": "FNN"
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "spark-vpc-v1",
  • "description": "Virtual network for machines executing Spark jobs v1",
  • "org": "xskkpgqpeakn",
  • "tenantId": "34f5c98e-f430-457b-a812-92637d0c6fd0",
  • "siteId": "72771e6a-6f5e-4de4-a5b9-1266c4197811",
  • "controllerVpcId": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "networkVirtualizationType": "FNN",
  • "requestedVni": 12001,
  • "vni": 12001,
  • "nvLinkLogicalPartitionId": "dd887330-dbd3-45ce-b400-c42fc8e47315",
  • "labels": {
    },
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

VPC Peering

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/vpc/{vpcId}/virtualization

Request samples

Content type
application/json
{
  • "networkVirtualizationType": "FNN"
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "spark-vpc-v1",
  • "description": "Virtual network for machines executing Spark jobs v1",
  • "org": "xskkpgqpeakn",
  • "tenantId": "34f5c98e-f430-457b-a812-92637d0c6fd0",
  • "siteId": "72771e6a-6f5e-4de4-a5b9-1266c4197811",
  • "controllerVpcId": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "networkVirtualizationType": "FNN",
  • "requestedVni": 12001,
  • "vni": 12001,
  • "nvLinkLogicalPartitionId": "dd887330-dbd3-45ce-b400-c42fc8e47315",
  • "labels": {
    },
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

VPC Peering

VPC Peering allows Instances in one VPC to communicate with Instances in another VPC on the same Site.

Retrieve all VPC peerings

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
[
  • {
    }
]

Create VPC peering

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/vpc-peering

Response samples

Content type
application/json
[
  • {
    }
]

Create VPC peering

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when requested object is not found

Request samples

Content type
application/json
{
  • "vpc1Id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "vpc2Id": "34f5c98e-f430-457b-a812-92637d0c6fd0",
  • "siteId": "72771e6a-6f5e-4de4-a5b9-1266c4197811"
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "vpc1Id": "bafbaf4c-c730-48ae-8f5d-d7d251f3315a",
  • "vpc1": {
    },
  • "vpc2Id": "a7cd678e-fe62-4184-9f75-66ac903ae1c3",
  • "vpc2": {
    },
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "site": {
    },
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "tenant": {
    },
  • "isMultiTenant": true,
  • "status": "Pending",
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve a VPC peering

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/vpc-peering

Request samples

Content type
application/json
{
  • "vpc1Id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "vpc2Id": "34f5c98e-f430-457b-a812-92637d0c6fd0",
  • "siteId": "72771e6a-6f5e-4de4-a5b9-1266c4197811"
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "vpc1Id": "bafbaf4c-c730-48ae-8f5d-d7d251f3315a",
  • "vpc1": {
    },
  • "vpc2Id": "a7cd678e-fe62-4184-9f75-66ac903ae1c3",
  • "vpc2": {
    },
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "site": {
    },
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "tenant": {
    },
  • "isMultiTenant": true,
  • "status": "Pending",
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve a VPC peering

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when requested object is not found

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "vpc1Id": "bafbaf4c-c730-48ae-8f5d-d7d251f3315a",
  • "vpc1": {
    },
  • "vpc2Id": "a7cd678e-fe62-4184-9f75-66ac903ae1c3",
  • "vpc2": {
    },
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "site": {
    },
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "tenant": {
    },
  • "isMultiTenant": true,
  • "status": "Pending",
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete a VPC peering

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/vpc-peering/{id}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "vpc1Id": "bafbaf4c-c730-48ae-8f5d-d7d251f3315a",
  • "vpc1": {
    },
  • "vpc2Id": "a7cd678e-fe62-4184-9f75-66ac903ae1c3",
  • "vpc2": {
    },
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "site": {
    },
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "tenant": {
    },
  • "isMultiTenant": true,
  • "status": "Pending",
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete a VPC peering

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when requested object is not found

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "Error validating request data",
  • "data": {
    }
}

VPC Prefix

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/vpc-peering/{id}

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "Error validating request data",
  • "data": {
    }
}

VPC Prefix

VPC Prefix is a network prefix belonging to an IP Block allocated to a Tenant. Tenant can use VPC Prefixes to enable network connectivity between their Instances.

Only Sites that support Native Networking (FNN) offer VPC Prefix management.

@@ -3392,7 +3422,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
[
  • {
    }
]

Create VPC Prefix

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/vpc-prefix

Response samples

Content type
application/json
[
  • {
    }
]

Create VPC Prefix

Create a VPC Prefix for the org.

Org must have a Tenant entity. User must have authorization role with TENANT_ADMIN suffix.

@@ -3456,7 +3486,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
{
  • "name": "east-vpc-traffic-net",
  • "vpcId": "5e28ad7c-5fb7-46d6-a28a-fc0ba6fdc4a3",
  • "ipBlockId": "8c1d1a06-90a2-4863-8ee1-6029265b9f0a",
  • "prefixLength": 20
}

Response samples

Content type
application/json
{
  • "id": "0c03ba01-d86b-4a57-a41e-cc359b380a6f",
  • "name": "east-vpc-traffic-net",
  • "siteId": "ea144def-d68f-44c3-9485-4b103fa2686f",
  • "vpcId": "5e28ad7c-5fb7-46d6-a28a-fc0ba6fdc4a3",
  • "prefix": "192.168.1.0/24",
  • "ipBlockId": "8c1d1a06-90a2-4863-8ee1-6029265b9f0a",
  • "prefixLength": 24,
  • "status": "Ready",
  • "usageStats": {
    },
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve VPC Prefix

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/vpc-prefix

Request samples

Content type
application/json
{
  • "name": "east-vpc-traffic-net",
  • "vpcId": "5e28ad7c-5fb7-46d6-a28a-fc0ba6fdc4a3",
  • "ipBlockId": "8c1d1a06-90a2-4863-8ee1-6029265b9f0a",
  • "prefixLength": 20
}

Response samples

Content type
application/json
{
  • "id": "0c03ba01-d86b-4a57-a41e-cc359b380a6f",
  • "name": "east-vpc-traffic-net",
  • "siteId": "ea144def-d68f-44c3-9485-4b103fa2686f",
  • "vpcId": "5e28ad7c-5fb7-46d6-a28a-fc0ba6fdc4a3",
  • "prefix": "192.168.1.0/24",
  • "ipBlockId": "8c1d1a06-90a2-4863-8ee1-6029265b9f0a",
  • "prefixLength": 24,
  • "status": "Ready",
  • "usageStats": {
    },
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve VPC Prefix

Retrieve a specific VPC Prefix

Org must have a Tenant entity. User must have authorization role with TENANT_ADMIN suffix.

@@ -3518,7 +3548,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
{
  • "id": "0c03ba01-d86b-4a57-a41e-cc359b380a6f",
  • "name": "east-vpc-traffic-net",
  • "siteId": "ea144def-d68f-44c3-9485-4b103fa2686f",
  • "vpcId": "5e28ad7c-5fb7-46d6-a28a-fc0ba6fdc4a3",
  • "prefix": "192.168.1.0/24",
  • "ipBlockId": "8c1d1a06-90a2-4863-8ee1-6029265b9f0a",
  • "prefixLength": 24,
  • "status": "Ready",
  • "usageStats": {
    },
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete VPC Prefix

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/vpc-prefix/{vpcPrefixId}

Response samples

Content type
application/json
{
  • "id": "0c03ba01-d86b-4a57-a41e-cc359b380a6f",
  • "name": "east-vpc-traffic-net",
  • "siteId": "ea144def-d68f-44c3-9485-4b103fa2686f",
  • "vpcId": "5e28ad7c-5fb7-46d6-a28a-fc0ba6fdc4a3",
  • "prefix": "192.168.1.0/24",
  • "ipBlockId": "8c1d1a06-90a2-4863-8ee1-6029265b9f0a",
  • "prefixLength": 24,
  • "status": "Ready",
  • "usageStats": {
    },
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete VPC Prefix

Delete a specific VPC Prefix by ID.

Org must have a Tenant entity. User must have authorization role with TENANT_ADMIN suffix.

@@ -3532,7 +3562,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "User is not allowed to perform this action",
  • "data": null
}

Update VPC Prefix

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/vpc-prefix/{vpcPrefixId}

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "User is not allowed to perform this action",
  • "data": null
}

Update VPC Prefix

Update an existing VPC Prefix

Org must have a Tenant entity. User must have authorization role with TENANT_ADMIN suffix.

@@ -3592,7 +3622,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
{
  • "name": "east-vpc-traffic-net"
}

Response samples

Content type
application/json
{
  • "id": "0c03ba01-d86b-4a57-a41e-cc359b380a6f",
  • "name": "east-vpc-traffic-net",
  • "siteId": "ea144def-d68f-44c3-9485-4b103fa2686f",
  • "vpcId": "5e28ad7c-5fb7-46d6-a28a-fc0ba6fdc4a3",
  • "prefix": "192.168.1.0/24",
  • "ipBlockId": "8c1d1a06-90a2-4863-8ee1-6029265b9f0a",
  • "prefixLength": 24,
  • "status": "Ready",
  • "usageStats": {
    },
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Subnet

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/vpc-prefix/{vpcPrefixId}

Request samples

Content type
application/json
{
  • "name": "east-vpc-traffic-net"
}

Response samples

Content type
application/json
{
  • "id": "0c03ba01-d86b-4a57-a41e-cc359b380a6f",
  • "name": "east-vpc-traffic-net",
  • "siteId": "ea144def-d68f-44c3-9485-4b103fa2686f",
  • "vpcId": "5e28ad7c-5fb7-46d6-a28a-fc0ba6fdc4a3",
  • "prefix": "192.168.1.0/24",
  • "ipBlockId": "8c1d1a06-90a2-4863-8ee1-6029265b9f0a",
  • "prefixLength": 24,
  • "status": "Ready",
  • "usageStats": {
    },
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Subnet

Subnet is a network prefix belonging to an IP Block allocated to a Tenant. Tenant can use Subnets to enable network connectivity between their Instances.

Subnets are used on Sites that do not support Native Networking (FNN).

@@ -3688,7 +3718,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
[
  • {
    }
]

Create Subnet

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/subnet

Response samples

Content type
application/json
[
  • {
    }
]

Create Subnet

Create a Subnet for the org.

@@ -3774,7 +3804,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
{
  • "name": "spark-gpu-net",
  • "vpcId": "5e28ad7c-5fb7-46d6-a28a-fc0ba6fdc4a3",
  • "ipv4BlockId": "8c1d1a06-90a2-4863-8ee1-6029265b9f0a",
  • "prefixLength": 20
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "spark-gpu-net",
  • "siteId": "ea144def-d68f-44c3-9485-4b103fa2686f",
  • "vpcId": "5e28ad7c-5fb7-46d6-a28a-fc0ba6fdc4a3",
  • "controllerNetworkSegmentId": null,
  • "ipv4Prefix": "202.168.16.0",
  • "ipv4BlockId": "8c1d1a06-90a2-4863-8ee1-6029265b9f0a",
  • "ipv4Gateway": "202.168.0.1",
  • "ipv6Prefix": null,
  • "ipv6BlockId": null,
  • "ipv6Gateway": null,
  • "prefixLength": 20,
  • "routingType": "Public",
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve Subnet

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/subnet

Request samples

Content type
application/json
{
  • "name": "spark-gpu-net",
  • "vpcId": "5e28ad7c-5fb7-46d6-a28a-fc0ba6fdc4a3",
  • "ipv4BlockId": "8c1d1a06-90a2-4863-8ee1-6029265b9f0a",
  • "prefixLength": 20
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "spark-gpu-net",
  • "siteId": "ea144def-d68f-44c3-9485-4b103fa2686f",
  • "vpcId": "5e28ad7c-5fb7-46d6-a28a-fc0ba6fdc4a3",
  • "controllerNetworkSegmentId": null,
  • "ipv4Prefix": "202.168.16.0",
  • "ipv4BlockId": "8c1d1a06-90a2-4863-8ee1-6029265b9f0a",
  • "ipv4Gateway": "202.168.0.1",
  • "ipv6Prefix": null,
  • "ipv6BlockId": null,
  • "ipv6Gateway": null,
  • "prefixLength": 20,
  • "routingType": "Public",
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve Subnet

Retrieve a specific Subnet

Org must have a Tenant entity. User must have authorization role with TENANT_ADMIN suffix.

@@ -3852,7 +3882,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "spark-gpu-net",
  • "siteId": "ea144def-d68f-44c3-9485-4b103fa2686f",
  • "vpcId": "5e28ad7c-5fb7-46d6-a28a-fc0ba6fdc4a3",
  • "controllerNetworkSegmentId": "abe7b0e8-67db-4e89-903e-fc4f2bd7f034",
  • "ipv4Prefix": "202.168.16.0",
  • "ipv4BlockId": "8c1d1a06-90a2-4863-8ee1-6029265b9f0a",
  • "ipv4Gateway": "202.168.0.1",
  • "ipv6Prefix": null,
  • "ipv6BlockId": null,
  • "ipv6Gateway": null,
  • "prefixLength": 20,
  • "routingType": "Public",
  • "status": "Ready",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete Subnet

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/subnet/{subnetId}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "spark-gpu-net",
  • "siteId": "ea144def-d68f-44c3-9485-4b103fa2686f",
  • "vpcId": "5e28ad7c-5fb7-46d6-a28a-fc0ba6fdc4a3",
  • "controllerNetworkSegmentId": "abe7b0e8-67db-4e89-903e-fc4f2bd7f034",
  • "ipv4Prefix": "202.168.16.0",
  • "ipv4BlockId": "8c1d1a06-90a2-4863-8ee1-6029265b9f0a",
  • "ipv4Gateway": "202.168.0.1",
  • "ipv6Prefix": null,
  • "ipv6BlockId": null,
  • "ipv6Gateway": null,
  • "prefixLength": 20,
  • "routingType": "Public",
  • "status": "Ready",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete Subnet

Delete a specific Subnet by ID.

Org must have a Tenant entity. User must have authorization role with TENANT_ADMIN suffix.

@@ -3866,7 +3896,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "User is not allowed to perform this action",
  • "data": null
}

Update Subnet

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/subnet/{subnetId}

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "User is not allowed to perform this action",
  • "data": null
}

Update Subnet

Update an existing Subnet

Org must have a Tenant entity. User must have authorization role with TENANT_ADMIN suffix.

@@ -3944,7 +3974,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
{
  • "name": "spark-gpu-subnet",
  • "description": "Subnet for dedicated GPU nodes"
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "spark-gpu-subnet",
  • "description": "Subnet for dedicated GPU nodes",
  • "siteId": "ea144def-d68f-44c3-9485-4b103fa2686f",
  • "vpcId": "5e28ad7c-5fb7-46d6-a28a-fc0ba6fdc4a3",
  • "controllerNetworkSegmentId": null,
  • "ipv4Prefix": "212.168.0.250",
  • "ipv4BlockId": "8c1d1a06-90a2-4863-8ee1-6029265b9f0a",
  • "ipv4Gateway": "202.168.0.1",
  • "ipv6Prefix": null,
  • "ipv6BlockId": null,
  • "ipv6Gateway": null,
  • "prefixLength": 20,
  • "routingType": "Public",
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Expected Machine

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/subnet/{subnetId}

Request samples

Content type
application/json
{
  • "name": "spark-gpu-subnet",
  • "description": "Subnet for dedicated GPU nodes"
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "spark-gpu-subnet",
  • "description": "Subnet for dedicated GPU nodes",
  • "siteId": "ea144def-d68f-44c3-9485-4b103fa2686f",
  • "vpcId": "5e28ad7c-5fb7-46d6-a28a-fc0ba6fdc4a3",
  • "controllerNetworkSegmentId": null,
  • "ipv4Prefix": "212.168.0.250",
  • "ipv4BlockId": "8c1d1a06-90a2-4863-8ee1-6029265b9f0a",
  • "ipv4Gateway": "202.168.0.1",
  • "ipv6Prefix": null,
  • "ipv6BlockId": null,
  • "ipv6Gateway": null,
  • "prefixLength": 20,
  • "routingType": "Public",
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Expected Machine

Expected Machine identifies a Machine that is expected to be discovered at a Site. Infrastructure Providers can pre-register Expected Machines using BMC credentials and serial numbers to help with Machine discovery and ingestion.

@@ -4100,7 +4130,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
{
  • "siteId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "bmcMacAddress": "00:1A:2B:3C:4D:5E",
  • "defaultBmcUsername": "admin",
  • "defaultBmcPassword": "password123",
  • "chassisSerialNumber": "CHASSIS-12345",
  • "fallbackDPUSerialNumbers": [
    ],
  • "labels": {
    }
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "siteId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "bmcMacAddress": "00:1A:2B:3C:4D:5E",
  • "bmcIpAddress": "192.168.1.100",
  • "chassisSerialNumber": "CHASSIS-12345",
  • "fallbackDPUSerialNumbers": [
    ],
  • "skuId": "lenovo.sr650v2.cpu.1",
  • "machineId": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
  • "rackId": "rack-01",
  • "isDpfEnabled": true,
  • "manufacturer": "Lenovo",
  • "labels": {
    },
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve all Expected Machines

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/expected-machine

Request samples

Content type
application/json
{
  • "siteId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "bmcMacAddress": "00:1A:2B:3C:4D:5E",
  • "defaultBmcUsername": "admin",
  • "defaultBmcPassword": "password123",
  • "chassisSerialNumber": "CHASSIS-12345",
  • "fallbackDPUSerialNumbers": [
    ],
  • "labels": {
    }
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "siteId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "bmcMacAddress": "00:1A:2B:3C:4D:5E",
  • "bmcIpAddress": "192.168.1.100",
  • "chassisSerialNumber": "CHASSIS-12345",
  • "fallbackDPUSerialNumbers": [
    ],
  • "skuId": "lenovo.sr650v2.cpu.1",
  • "machineId": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
  • "rackId": "rack-01",
  • "isDpfEnabled": true,
  • "manufacturer": "Lenovo",
  • "labels": {
    },
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve all Expected Machines

Retrieve all Expected Machines.

@@ -4206,7 +4236,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve Expected Machine

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/expected-machine

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve Expected Machine

Retrieve a specific Expected Machine by ID.

@@ -4322,7 +4352,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when requested object is not found

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "siteId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "bmcMacAddress": "00:1A:2B:3C:4D:5E",
  • "bmcIpAddress": "192.168.1.100",
  • "chassisSerialNumber": "CHASSIS-12345",
  • "fallbackDPUSerialNumbers": [
    ],
  • "skuId": "lenovo.sr650v2.cpu.1",
  • "machineId": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
  • "rackId": "rack-01",
  • "isDpfEnabled": true,
  • "manufacturer": "Lenovo",
  • "labels": {
    },
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Update Expected Machine

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/expected-machine/{expectedMachineId}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "siteId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "bmcMacAddress": "00:1A:2B:3C:4D:5E",
  • "bmcIpAddress": "192.168.1.100",
  • "chassisSerialNumber": "CHASSIS-12345",
  • "fallbackDPUSerialNumbers": [
    ],
  • "skuId": "lenovo.sr650v2.cpu.1",
  • "machineId": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
  • "rackId": "rack-01",
  • "isDpfEnabled": true,
  • "manufacturer": "Lenovo",
  • "labels": {
    },
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Update Expected Machine

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when requested object is not found

Request samples

Content type
application/json
{
  • "defaultBmcUsername": "newadmin",
  • "defaultBmcPassword": "newpassword123",
  • "chassisSerialNumber": "CHASSIS-54321",
  • "labels": {
    }
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "siteId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "bmcMacAddress": "00:1A:2B:3C:4D:5E",
  • "bmcIpAddress": "192.168.1.100",
  • "chassisSerialNumber": "CHASSIS-12345",
  • "fallbackDPUSerialNumbers": [
    ],
  • "skuId": "lenovo.sr650v2.cpu.1",
  • "machineId": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
  • "rackId": "rack-01",
  • "isDpfEnabled": true,
  • "manufacturer": "Lenovo",
  • "labels": {
    },
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete Expected Machine

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/expected-machine/{expectedMachineId}

Request samples

Content type
application/json
{
  • "defaultBmcUsername": "newadmin",
  • "defaultBmcPassword": "newpassword123",
  • "chassisSerialNumber": "CHASSIS-54321",
  • "labels": {
    }
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "siteId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "bmcMacAddress": "00:1A:2B:3C:4D:5E",
  • "bmcIpAddress": "192.168.1.100",
  • "chassisSerialNumber": "CHASSIS-12345",
  • "fallbackDPUSerialNumbers": [
    ],
  • "skuId": "lenovo.sr650v2.cpu.1",
  • "machineId": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
  • "rackId": "rack-01",
  • "isDpfEnabled": true,
  • "manufacturer": "Lenovo",
  • "labels": {
    },
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete Expected Machine

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when requested object is not found

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "Error validating request data",
  • "data": {
    }
}

Batch Create Expected Machines

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/expected-machine/{expectedMachineId}

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "Error validating request data",
  • "data": {
    }
}

Batch Create Expected Machines

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
[
  • {
    },
  • {
    }
]

Response samples

Content type
application/json
[
  • {
    }
]

Batch Update Expected Machines

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/expected-machine/batch

Request samples

Content type
application/json
[
  • {
    },
  • {
    }
]

Response samples

Content type
application/json
[
  • {
    }
]

Batch Update Expected Machines

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

One or more Expected Machines not found

Request samples

Content type
application/json
[
  • {
    },
  • {
    }
]

Response samples

Content type
application/json
[
  • {
    }
]

Expected Power Shelf

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/expected-machine/batch

Request samples

Content type
application/json
[
  • {
    },
  • {
    }
]

Response samples

Content type
application/json
[
  • {
    }
]

Expected Power Shelf

Expected Power Shelf identifies a Power Shelf that is expected to be discovered at a Site. Infrastructure Providers can pre-register Expected Power Shelves using BMC credentials and serial numbers to help with Power Shelf discovery and ingestion.

@@ -4874,7 +4904,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
{
  • "siteId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "bmcMacAddress": "00:1A:2B:3C:4D:5E",
  • "defaultBmcUsername": "admin",
  • "defaultBmcPassword": "password123",
  • "shelfSerialNumber": "SHELF-12345",
  • "bmcIpAddress": "192.168.1.100",
  • "labels": {
    }
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "siteId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "bmcMacAddress": "00:1A:2B:3C:4D:5E",
  • "shelfSerialNumber": "SHELF-12345",
  • "bmcIpAddress": "192.168.1.100",
  • "rackId": "rack-01",
  • "manufacturer": "Delta",
  • "labels": {
    },
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve all Expected Power Shelves

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/expected-power-shelf

Request samples

Content type
application/json
{
  • "siteId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "bmcMacAddress": "00:1A:2B:3C:4D:5E",
  • "defaultBmcUsername": "admin",
  • "defaultBmcPassword": "password123",
  • "shelfSerialNumber": "SHELF-12345",
  • "bmcIpAddress": "192.168.1.100",
  • "labels": {
    }
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "siteId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "bmcMacAddress": "00:1A:2B:3C:4D:5E",
  • "shelfSerialNumber": "SHELF-12345",
  • "bmcIpAddress": "192.168.1.100",
  • "rackId": "rack-01",
  • "manufacturer": "Delta",
  • "labels": {
    },
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve all Expected Power Shelves

Retrieve all Expected Power Shelves.

@@ -4934,7 +4964,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve Expected Power Shelf

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/expected-power-shelf

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve Expected Power Shelf

Retrieve a specific Expected Power Shelf by ID.

@@ -4988,7 +5018,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when requested object is not found

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "siteId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "bmcMacAddress": "00:1A:2B:3C:4D:5E",
  • "shelfSerialNumber": "SHELF-12345",
  • "bmcIpAddress": "192.168.1.100",
  • "rackId": "rack-01",
  • "manufacturer": "Delta",
  • "labels": {
    },
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Update Expected Power Shelf

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/expected-power-shelf/{expectedPowerShelfId}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "siteId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "bmcMacAddress": "00:1A:2B:3C:4D:5E",
  • "shelfSerialNumber": "SHELF-12345",
  • "bmcIpAddress": "192.168.1.100",
  • "rackId": "rack-01",
  • "manufacturer": "Delta",
  • "labels": {
    },
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Update Expected Power Shelf

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when requested object is not found

Request samples

Content type
application/json
{
  • "defaultBmcUsername": "newadmin",
  • "defaultBmcPassword": "newpassword123",
  • "shelfSerialNumber": "SHELF-54321",
  • "bmcIpAddress": "192.168.1.200",
  • "labels": {
    }
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "siteId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "bmcMacAddress": "00:1A:2B:3C:4D:5E",
  • "shelfSerialNumber": "SHELF-12345",
  • "bmcIpAddress": "192.168.1.100",
  • "rackId": "rack-01",
  • "manufacturer": "Delta",
  • "labels": {
    },
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete Expected Power Shelf

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/expected-power-shelf/{expectedPowerShelfId}

Request samples

Content type
application/json
{
  • "defaultBmcUsername": "newadmin",
  • "defaultBmcPassword": "newpassword123",
  • "shelfSerialNumber": "SHELF-54321",
  • "bmcIpAddress": "192.168.1.200",
  • "labels": {
    }
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "siteId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "bmcMacAddress": "00:1A:2B:3C:4D:5E",
  • "shelfSerialNumber": "SHELF-12345",
  • "bmcIpAddress": "192.168.1.100",
  • "rackId": "rack-01",
  • "manufacturer": "Delta",
  • "labels": {
    },
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete Expected Power Shelf

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when requested object is not found

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "Error validating request data",
  • "data": {
    }
}

Expected Rack

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/expected-power-shelf/{expectedPowerShelfId}

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "Error validating request data",
  • "data": {
    }
}

Expected Rack

Expected Rack identifies a Rack that is expected to be discovered at a Site. Infrastructure Providers can pre-register Expected Racks with an operator-supplied @@ -5160,7 +5190,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
{
  • "siteId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "rackId": "rack-01",
  • "rackProfileId": "rp-standard-42u",
  • "name": "Rack 01",
  • "description": "Production rack in row A",
  • "labels": {
    }
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "rackId": "rack-01",
  • "siteId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "rackProfileId": "rp-standard-42u",
  • "name": "Rack 01",
  • "description": "Production rack in row A",
  • "labels": {
    },
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve all Expected Racks

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/expected-rack

Request samples

Content type
application/json
{
  • "siteId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "rackId": "rack-01",
  • "rackProfileId": "rp-standard-42u",
  • "name": "Rack 01",
  • "description": "Production rack in row A",
  • "labels": {
    }
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "rackId": "rack-01",
  • "siteId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "rackProfileId": "rp-standard-42u",
  • "name": "Rack 01",
  • "description": "Production rack in row A",
  • "labels": {
    },
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve all Expected Racks

Retrieve all Expected Racks.

@@ -5206,7 +5236,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
[
  • {
    }
]

Replace all Expected Racks

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/expected-rack

Response samples

Content type
application/json
[
  • {
    }
]

Replace all Expected Racks

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
{
  • "siteId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "expectedRacks": [
    ]
}

Response samples

Content type
application/json
[
  • {
    }
]

Delete all Expected Racks

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/expected-rack

Request samples

Content type
application/json
{
  • "siteId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "expectedRacks": [
    ]
}

Response samples

Content type
application/json
[
  • {
    }
]

Delete all Expected Racks

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "Error validating request data",
  • "data": {
    }
}

Retrieve Expected Rack

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/expected-rack/all

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "Error validating request data",
  • "data": {
    }
}

Retrieve Expected Rack

Retrieve a specific Expected Rack by its id.

@@ -5320,7 +5350,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when requested object is not found

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "rackId": "rack-01",
  • "siteId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "rackProfileId": "rp-standard-42u",
  • "name": "Rack 01",
  • "description": "Production rack in row A",
  • "labels": {
    },
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Update Expected Rack

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/expected-rack/{id}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "rackId": "rack-01",
  • "siteId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "rackProfileId": "rp-standard-42u",
  • "name": "Rack 01",
  • "description": "Production rack in row A",
  • "labels": {
    },
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Update Expected Rack

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when requested object is not found

Request samples

Content type
application/json
{
  • "rackProfileId": "rp-standard-48u",
  • "name": "Rack 01 (updated)",
  • "description": "Production rack in row A, upgraded chassis",
  • "labels": {
    }
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "rackId": "rack-01",
  • "siteId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "rackProfileId": "rp-standard-42u",
  • "name": "Rack 01",
  • "description": "Production rack in row A",
  • "labels": {
    },
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete Expected Rack

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/expected-rack/{id}

Request samples

Content type
application/json
{
  • "rackProfileId": "rp-standard-48u",
  • "name": "Rack 01 (updated)",
  • "description": "Production rack in row A, upgraded chassis",
  • "labels": {
    }
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "rackId": "rack-01",
  • "siteId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "rackProfileId": "rp-standard-42u",
  • "name": "Rack 01",
  • "description": "Production rack in row A",
  • "labels": {
    },
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete Expected Rack

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when requested object is not found

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "Error validating request data",
  • "data": {
    }
}

Expected Switch

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/expected-rack/{id}

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "Error validating request data",
  • "data": {
    }
}

Expected Switch

Expected Switch identifies an NVLink Switch that is expected to be discovered at a Site. Infrastructure Providers can pre-register Expected Switches using BMC, NVOS credentials and serial numbers to help with NVLink Switch discovery and ingestion.

@@ -5488,7 +5518,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
{
  • "siteId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "bmcMacAddress": "00:1A:2B:3C:4D:5E",
  • "defaultBmcUsername": "admin",
  • "defaultBmcPassword": "password123",
  • "switchSerialNumber": "SWITCH-12345",
  • "nvOsUsername": "nvadmin",
  • "nvOsPassword": "nvpassword123",
  • "nvosMacAddresses": [
    ],
  • "labels": {
    }
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "siteId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "bmcMacAddress": "00:1A:2B:3C:4D:5E",
  • "bmcIpAddress": "192.168.1.100",
  • "switchSerialNumber": "SWITCH-12345",
  • "nvosMacAddresses": [
    ],
  • "rackId": "rack-01",
  • "manufacturer": "NVIDIA",
  • "labels": {
    },
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve all Expected Switches

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/expected-switch

Request samples

Content type
application/json
{
  • "siteId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "bmcMacAddress": "00:1A:2B:3C:4D:5E",
  • "defaultBmcUsername": "admin",
  • "defaultBmcPassword": "password123",
  • "switchSerialNumber": "SWITCH-12345",
  • "nvOsUsername": "nvadmin",
  • "nvOsPassword": "nvpassword123",
  • "nvosMacAddresses": [
    ],
  • "labels": {
    }
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "siteId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "bmcMacAddress": "00:1A:2B:3C:4D:5E",
  • "bmcIpAddress": "192.168.1.100",
  • "switchSerialNumber": "SWITCH-12345",
  • "nvosMacAddresses": [
    ],
  • "rackId": "rack-01",
  • "manufacturer": "NVIDIA",
  • "labels": {
    },
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve all Expected Switches

Retrieve all Expected Switches.

@@ -5550,7 +5580,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve Expected Switch

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/expected-switch

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve Expected Switch

Retrieve a specific Expected Switch by ID.

@@ -5606,7 +5636,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when requested object is not found

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "siteId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "bmcMacAddress": "00:1A:2B:3C:4D:5E",
  • "bmcIpAddress": "192.168.1.100",
  • "switchSerialNumber": "SWITCH-12345",
  • "nvosMacAddresses": [
    ],
  • "rackId": "rack-01",
  • "manufacturer": "NVIDIA",
  • "labels": {
    },
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Update Expected Switch

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/expected-switch/{expectedSwitchId}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "siteId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "bmcMacAddress": "00:1A:2B:3C:4D:5E",
  • "bmcIpAddress": "192.168.1.100",
  • "switchSerialNumber": "SWITCH-12345",
  • "nvosMacAddresses": [
    ],
  • "rackId": "rack-01",
  • "manufacturer": "NVIDIA",
  • "labels": {
    },
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Update Expected Switch

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when requested object is not found

Request samples

Content type
application/json
{
  • "defaultBmcUsername": "newadmin",
  • "defaultBmcPassword": "newpassword123",
  • "switchSerialNumber": "SWITCH-54321",
  • "nvOsUsername": "newnvadmin",
  • "nvOsPassword": "newnvpassword123",
  • "nvosMacAddresses": [
    ],
  • "labels": {
    }
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "siteId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "bmcMacAddress": "00:1A:2B:3C:4D:5E",
  • "bmcIpAddress": "192.168.1.100",
  • "switchSerialNumber": "SWITCH-12345",
  • "nvosMacAddresses": [
    ],
  • "rackId": "rack-01",
  • "manufacturer": "NVIDIA",
  • "labels": {
    },
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete Expected Switch

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/expected-switch/{expectedSwitchId}

Request samples

Content type
application/json
{
  • "defaultBmcUsername": "newadmin",
  • "defaultBmcPassword": "newpassword123",
  • "switchSerialNumber": "SWITCH-54321",
  • "nvOsUsername": "newnvadmin",
  • "nvOsPassword": "newnvpassword123",
  • "nvosMacAddresses": [
    ],
  • "labels": {
    }
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "siteId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "bmcMacAddress": "00:1A:2B:3C:4D:5E",
  • "bmcIpAddress": "192.168.1.100",
  • "switchSerialNumber": "SWITCH-12345",
  • "nvosMacAddresses": [
    ],
  • "rackId": "rack-01",
  • "manufacturer": "NVIDIA",
  • "labels": {
    },
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete Expected Switch

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when requested object is not found

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "Error validating request data",
  • "data": {
    }
}

SKU

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/expected-switch/{expectedSwitchId}

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "Error validating request data",
  • "data": {
    }
}

SKU

SKU (Stock Keeping Unit) defines one or more hardware configurations or Machine Bill of Materials (BOM).

SKUs are automatically derived from machine hardware characteristics and used to group similar machines. SKUs are read-only and managed by the system.

@@ -5786,7 +5816,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve SKU

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/sku

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve SKU

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when requested object is not found

Response samples

Content type
application/json
{
  • "id": "lenovo.sr650v2.cpu.1",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "deviceType": "gpu",
  • "associatedMachineIds": [
    ],
  • "components": {
    },
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

InfiniBand Partition

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/sku/{skuId}

Response samples

Content type
application/json
{
  • "id": "lenovo.sr650v2.cpu.1",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "deviceType": "gpu",
  • "associatedMachineIds": [
    ],
  • "components": {
    },
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

InfiniBand Partition

InfiniBand (IB) is a high-performance, low-latency networking standard designed for interconnecting servers and storage in HPC (High-Performance Computing) and AI systems, utilizing RDMA (Remote Direct Memory Access) to reduce CPU overhead. InfiniBand Partitions are used to group Machines into logical partitions for network isolation and load distribution.

@@ -5962,7 +5992,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
[
  • {
    }
]

Create InfiniBand Partition

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/infiniband-partition

Response samples

Content type
application/json
[
  • {
    }
]

Create InfiniBand Partition

Create an InfiniBand Partition for the org.

Org must have a Tenant entity. User must have authorization role with TENANT_ADMIN suffix.

@@ -6026,7 +6056,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
{
  • "name": "turbo-net",
  • "siteId": "69dae3c8-3554-4a1f-b391-858c6dc47fff",
  • "labels": {
    }
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "turbo-net",
  • "description": "InfiniBand Partition for model training Instances",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "controllerIBPartitionId": "0e60d064-3d38-4812-84d9-c3353bd96eaf",
  • "partitionKey": "0x1",
  • "partitionName": "turbo-net",
  • "serviceLevel": 5,
  • "rateLimit": 40,
  • "mtu": 4000,
  • "enableSharp": true,
  • "labels": {
    },
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve InfiniBand Partition

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/infiniband-partition

Request samples

Content type
application/json
{
  • "name": "turbo-net",
  • "siteId": "69dae3c8-3554-4a1f-b391-858c6dc47fff",
  • "labels": {
    }
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "turbo-net",
  • "description": "InfiniBand Partition for model training Instances",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "controllerIBPartitionId": "0e60d064-3d38-4812-84d9-c3353bd96eaf",
  • "partitionKey": "0x1",
  • "partitionName": "turbo-net",
  • "serviceLevel": 5,
  • "rateLimit": 40,
  • "mtu": 4000,
  • "enableSharp": true,
  • "labels": {
    },
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve InfiniBand Partition

Retrieve a specific InfiniBand Partition

@@ -6086,7 +6116,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "turbo-net",
  • "description": "InfiniBand Partition for model training Instances",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "controllerIBPartitionId": "0e60d064-3d38-4812-84d9-c3353bd96eaf",
  • "partitionKey": "0x1",
  • "partitionName": "turbo-net",
  • "serviceLevel": 5,
  • "rateLimit": 40,
  • "mtu": 4000,
  • "enableSharp": true,
  • "labels": {
    },
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete InfiniBand Partition

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/infiniband-partition/{infiniBandPartitionId}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "turbo-net",
  • "description": "InfiniBand Partition for model training Instances",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "controllerIBPartitionId": "0e60d064-3d38-4812-84d9-c3353bd96eaf",
  • "partitionKey": "0x1",
  • "partitionName": "turbo-net",
  • "serviceLevel": 5,
  • "rateLimit": 40,
  • "mtu": 4000,
  • "enableSharp": true,
  • "labels": {
    },
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete InfiniBand Partition

Delete a specific InfiniBand Partition by ID.

@@ -6102,7 +6132,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

Update InfiniBand Partition

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/infiniband-partition/{infiniBandPartitionId}

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

Update InfiniBand Partition

Update an existing InfiniBand Partition

@@ -6168,7 +6198,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
{
  • "name": "turbo-net-v2",
  • "description": "Second version of the model training network",
  • "labels": {
    }
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "turbo-net-v2",
  • "description": "Second version of the model training network",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "controllerIBPartitionId": "0e60d064-3d38-4812-84d9-c3353bd96eaf",
  • "partitionKey": "0x1",
  • "partitionName": "turbo-net",
  • "serviceLevel": 5,
  • "rateLimit": 40,
  • "mtu": 4000,
  • "enableSharp": true,
  • "labels": {
    },
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve all InfiniBand Interfaces

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/infiniband-partition/{infiniBandPartitionId}

Request samples

Content type
application/json
{
  • "name": "turbo-net-v2",
  • "description": "Second version of the model training network",
  • "labels": {
    }
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "turbo-net-v2",
  • "description": "Second version of the model training network",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "controllerIBPartitionId": "0e60d064-3d38-4812-84d9-c3353bd96eaf",
  • "partitionKey": "0x1",
  • "partitionName": "turbo-net",
  • "serviceLevel": 5,
  • "rateLimit": 40,
  • "mtu": 4000,
  • "enableSharp": true,
  • "labels": {
    },
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve all InfiniBand Interfaces

Get all InfiniBand Interfaces

Org must have a Tenant entity. User must have authorization role with TENANT_ADMIN suffix.

@@ -6222,7 +6252,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
[
  • {
    }
]

Response samples

Content type
application/json
[
  • {
    }
]

Response samples

Content type
application/json
[
  • {
    }
]

Operating System

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/nvlink-interface

Response samples

Content type
application/json
[
  • {
    }
]

Operating System

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
Example
[
  • {
    }
]

Create Operating System

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/operating-system

Response samples

Content type
application/json
Example
[
  • {
    }
]

Create Operating System

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
Example
{
  • "name": "ubuntu-official-22.04",
  • "description": "Official Ubuntu 22.04",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "ipxeScript": "#!ipxe\nkernel http://archive.ubuntu.com/ubuntu/dists/xenial/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64/linux initrd=initrd.gz\ninitrd http://archive.ubuntu.com/ubuntu/dists/xenial/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64/initrd.gz\nboot || imgfree\n shell",
  • "userData": "#cloud-config\nautoinstall:\n apt:\n geoip: true\n preserve_sources_list: false\n primary:\n - arches: [amd64, i386]\n uri: http://archive.ubuntu.com/ubuntu\n - arches: [default]\n uri: http://ports.ubuntu.com/ubuntu-ports",
  • "isCloudInit": true,
  • "phoneHomeEnabled": true,
  • "allowOverride": false
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "ubuntu-22.04",
  • "description": "Ubuntu 22.04",
  • "infrastructureProviderId": null,
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "type": "iPXE",
  • "ipxeScript": "#!ipxe\nkernel http://archive.ubuntu.com/ubuntu/dists/xenial/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64/linux initrd=initrd.gz\ninitrd http://archive.ubuntu.com/ubuntu/dists/xenial/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64/initrd.gz\nboot || imgfree\n shell",
  • "userData": "#cloud-config\nautoinstall:\n apt:\n geoip: true\n preserve_sources_list: false\n primary:\n - arches: [amd64, i386]\n uri: http://archive.ubuntu.com/ubuntu\n - arches: [default]\n uri: http://ports.ubuntu.com/ubuntu-ports",
  • "isCloudInit": true,
  • "phoneHomeEnabled": false,
  • "allowOverride": false,
  • "imageAuthToken": null,
  • "imageAuthType": null,
  • "imageDisk": null,
  • "imageSha": null,
  • "imageUrl": null,
  • "rootFsId": null,
  • "rootFsLabel": null,
  • "siteAssociations": [ ],
  • "isActive": true,
  • "deactivationNote": null,
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve Operating System

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/operating-system

Request samples

Content type
application/json
Example
{
  • "name": "ubuntu-official-22.04",
  • "description": "Official Ubuntu 22.04",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "ipxeScript": "#!ipxe\nkernel http://archive.ubuntu.com/ubuntu/dists/xenial/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64/linux initrd=initrd.gz\ninitrd http://archive.ubuntu.com/ubuntu/dists/xenial/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64/initrd.gz\nboot || imgfree\n shell",
  • "userData": "#cloud-config\nautoinstall:\n apt:\n geoip: true\n preserve_sources_list: false\n primary:\n - arches: [amd64, i386]\n uri: http://archive.ubuntu.com/ubuntu\n - arches: [default]\n uri: http://ports.ubuntu.com/ubuntu-ports",
  • "isCloudInit": true,
  • "phoneHomeEnabled": true,
  • "allowOverride": false
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "ubuntu-22.04",
  • "description": "Ubuntu 22.04",
  • "infrastructureProviderId": null,
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "type": "iPXE",
  • "ipxeScript": "#!ipxe\nkernel http://archive.ubuntu.com/ubuntu/dists/xenial/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64/linux initrd=initrd.gz\ninitrd http://archive.ubuntu.com/ubuntu/dists/xenial/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64/initrd.gz\nboot || imgfree\n shell",
  • "userData": "#cloud-config\nautoinstall:\n apt:\n geoip: true\n preserve_sources_list: false\n primary:\n - arches: [amd64, i386]\n uri: http://archive.ubuntu.com/ubuntu\n - arches: [default]\n uri: http://ports.ubuntu.com/ubuntu-ports",
  • "isCloudInit": true,
  • "phoneHomeEnabled": false,
  • "allowOverride": false,
  • "imageAuthToken": null,
  • "imageAuthType": null,
  • "imageDisk": null,
  • "imageSha": null,
  • "imageUrl": null,
  • "rootFsId": null,
  • "rootFsLabel": null,
  • "siteAssociations": [ ],
  • "isActive": true,
  • "deactivationNote": null,
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve Operating System

Get an Operating System by ID

@@ -6852,7 +6882,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
Example
{
  • "id": "42b0f982-5c61-4d2f-a018-41ece61f4641",
  • "name": "debian-12-amd64",
  • "description": "Official Debian 12 for AMD/Intel",
  • "infrastructureProviderId": null,
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "type": "Image",
  • "imageSha": "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
  • "imageAuthType": "Bearer",
  • "imageAuthToken": "acbd18db4cc2f85cedef654fccc4a4d8",
  • "imageDisk": "/dev/sda",
  • "rootFsId": "6c2ac315-3040-4728-94eb-b66d320206c1",
  • "rootFsLabel": null,
  • "ipxeScript": null,
  • "userData": null,
  • "isCloudInit": false,
  • "phoneHomeEnabled": false,
  • "allowOverride": false,
  • "siteAssociations": [
    ],
  • "isActive": true,
  • "deactivationNote": null,
  • "status": "Syncing",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete Operating System

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/operating-system/{operatingSystemId}

Response samples

Content type
application/json
Example
{
  • "id": "42b0f982-5c61-4d2f-a018-41ece61f4641",
  • "name": "debian-12-amd64",
  • "description": "Official Debian 12 for AMD/Intel",
  • "infrastructureProviderId": null,
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "type": "Image",
  • "imageSha": "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
  • "imageAuthType": "Bearer",
  • "imageAuthToken": "acbd18db4cc2f85cedef654fccc4a4d8",
  • "imageDisk": "/dev/sda",
  • "rootFsId": "6c2ac315-3040-4728-94eb-b66d320206c1",
  • "rootFsLabel": null,
  • "ipxeScript": null,
  • "userData": null,
  • "isCloudInit": false,
  • "phoneHomeEnabled": false,
  • "allowOverride": false,
  • "siteAssociations": [
    ],
  • "isActive": true,
  • "deactivationNote": null,
  • "status": "Syncing",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete Operating System

Delete an Operating System by ID

@@ -6868,7 +6898,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

Update Operating System

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/operating-system/{operatingSystemId}

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

Update Operating System

Update an Operating System by ID

@@ -6986,7 +7016,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
{
  • "name": "ubuntu-22.04-lts",
  • "description": "Ubuntu 22.04 LTS",
  • "allowOverride": true
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "ubuntu-22.04-lts",
  • "description": "Ubuntu 22.04 LTS",
  • "infrastructureProviderId": null,
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "type": "iPXE",
  • "ipxeScript": "#!ipxe\nkernel http://archive.ubuntu.com/ubuntu/dists/xenial/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64/linux initrd=initrd.gz\ninitrd http://archive.ubuntu.com/ubuntu/dists/xenial/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64/initrd.gz\nboot || imgfree\n shell",
  • "userData": "#cloud-config\nautoinstall:\n apt:\n geoip: true\n preserve_sources_list: false\n primary:\n - arches: [amd64, i386]\n uri: http://archive.ubuntu.com/ubuntu\n - arches: [default]\n uri: http://ports.ubuntu.com/ubuntu-ports",
  • "isCloudInit": true,
  • "allowOverride": true,
  • "phoneHomeEnabled": true,
  • "imageAuthToken": null,
  • "imageAuthType": null,
  • "imageDisk": null,
  • "imageSha": null,
  • "imageUrl": null,
  • "rootFsId": null,
  • "rootFsLabel": null,
  • "siteAssociations": [ ],
  • "isActive": true,
  • "deactivationNote": null,
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Instance Type

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/operating-system/{operatingSystemId}

Request samples

Content type
application/json
{
  • "name": "ubuntu-22.04-lts",
  • "description": "Ubuntu 22.04 LTS",
  • "allowOverride": true
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "ubuntu-22.04-lts",
  • "description": "Ubuntu 22.04 LTS",
  • "infrastructureProviderId": null,
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "type": "iPXE",
  • "ipxeScript": "#!ipxe\nkernel http://archive.ubuntu.com/ubuntu/dists/xenial/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64/linux initrd=initrd.gz\ninitrd http://archive.ubuntu.com/ubuntu/dists/xenial/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64/initrd.gz\nboot || imgfree\n shell",
  • "userData": "#cloud-config\nautoinstall:\n apt:\n geoip: true\n preserve_sources_list: false\n primary:\n - arches: [amd64, i386]\n uri: http://archive.ubuntu.com/ubuntu\n - arches: [default]\n uri: http://ports.ubuntu.com/ubuntu-ports",
  • "isCloudInit": true,
  • "allowOverride": true,
  • "phoneHomeEnabled": true,
  • "imageAuthToken": null,
  • "imageAuthType": null,
  • "imageDisk": null,
  • "imageSha": null,
  • "imageUrl": null,
  • "rootFsId": null,
  • "rootFsLabel": null,
  • "siteAssociations": [ ],
  • "isActive": true,
  • "deactivationNote": null,
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Instance Type

Instance Types allow grouping Machines into a pool defined by their capabilities. Providers can then allocate a portion of the Instance Type pool to a Tenant.

Retrieve all Instance Types

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
Example
[
  • {
    }
]

Create an Instance Type

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/instance/type

Response samples

Content type
application/json
Example
[
  • {
    }
]

Create an Instance Type

Create an Instance Type for Infrastructure Provider.

Org must have an Infrastructure Provider entity. User must have authorization role with PROVIDER_ADMIN suffix.

@@ -7262,7 +7292,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
Example
{
  • "name": "x3.large",
  • "description": "Part of X family, the X3 Large features increased compute power",
  • "siteId": "8d97fa69-9199-49ff-bcf3-168c62d3874e",
  • "labels": {
    },
  • "machineCapabilities": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "x3.large",
  • "description": "Part of X family, the X3 Large features increased compute power",
  • "infrastructureProviderId": "5f2cc306-76e9-4fca-9186-950c9ef9a74e",
  • "siteId": "72771e6a-6f5e-4de4-a5b9-1266c4197811",
  • "labels": {
    },
  • "machineCapabilities": [
    ],
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve an Instance Type

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/instance/type

Request samples

Content type
application/json
Example
{
  • "name": "x3.large",
  • "description": "Part of X family, the X3 Large features increased compute power",
  • "siteId": "8d97fa69-9199-49ff-bcf3-168c62d3874e",
  • "labels": {
    },
  • "machineCapabilities": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "x3.large",
  • "description": "Part of X family, the X3 Large features increased compute power",
  • "infrastructureProviderId": "5f2cc306-76e9-4fca-9186-950c9ef9a74e",
  • "siteId": "72771e6a-6f5e-4de4-a5b9-1266c4197811",
  • "labels": {
    },
  • "machineCapabilities": [
    ],
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve an Instance Type

Get an Instance Type by ID.

@@ -7380,7 +7410,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "x3.large",
  • "description": "Part of X family, the X3 Large features increased compute power",
  • "infrastructureProviderId": "5f2cc306-76e9-4fca-9186-950c9ef9a74e",
  • "siteId": "72771e6a-6f5e-4de4-a5b9-1266c4197811",
  • "labels": {
    },
  • "machineCapabilities": [
    ],
  • "allocationStats": {
    },
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete Instance Type

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/instance/type/{instanceTypeId}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "x3.large",
  • "description": "Part of X family, the X3 Large features increased compute power",
  • "infrastructureProviderId": "5f2cc306-76e9-4fca-9186-950c9ef9a74e",
  • "siteId": "72771e6a-6f5e-4de4-a5b9-1266c4197811",
  • "labels": {
    },
  • "machineCapabilities": [
    ],
  • "allocationStats": {
    },
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete Instance Type

Delete an Instance Type by ID.

Org must have an Infrastructure Provider entity that owns the Instance Type. User must have authorization role with PROVIDER_ADMIN suffix.

@@ -7394,7 +7424,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

Update Instance Type

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/instance/type/{instanceTypeId}

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

Update Instance Type

Update an Instance Type by ID.

Org must have an Infrastructure Provider entity that owns the Instance Type. User must have authorization role with PROVIDER_ADMIN suffix.

@@ -7536,7 +7566,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
{
  • "description": "Updated version of the X3 Large family of machines",
  • "labels": {
    },
  • "machineCapabilities": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "x3.large",
  • "description": "Updated version of the X3 Large family of machines",
  • "infrastructureProviderId": "5f2cc306-76e9-4fca-9186-950c9ef9a74e",
  • "siteId": "72771e6a-6f5e-4de4-a5b9-1266c4197811",
  • "labels": {
    },
  • "machineCapabilities": [
    ],
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve all Machines/Instance Type associations

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/instance/type/{instanceTypeId}

Request samples

Content type
application/json
{
  • "description": "Updated version of the X3 Large family of machines",
  • "labels": {
    },
  • "machineCapabilities": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "x3.large",
  • "description": "Updated version of the X3 Large family of machines",
  • "infrastructureProviderId": "5f2cc306-76e9-4fca-9186-950c9ef9a74e",
  • "siteId": "72771e6a-6f5e-4de4-a5b9-1266c4197811",
  • "labels": {
    },
  • "machineCapabilities": [
    ],
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve all Machines/Instance Type associations

Get all Machines for a given Instance Type

Org must have an Infrastructure Provider entity that owns the Instance Type and the Machine. User must have authorization role with PROVIDER_ADMIN suffix.

@@ -7582,7 +7612,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
[
  • {
    },
  • {
    }
]

Create a Machine/Instance Type association

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/instance/type/{instanceTypeId}/machine

Response samples

Content type
application/json
[
  • {
    },
  • {
    }
]

Create a Machine/Instance Type association

Associate a Machine to an Instance Type

@@ -7626,7 +7656,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
{
  • "machineIds": [
    ]
}

Response samples

Content type
application/json
[
  • {
    },
  • {
    },
  • {
    }
]

Delete a Machine/Instance Type association

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/instance/type/{instanceTypeId}/machine

Request samples

Content type
application/json
{
  • "machineIds": [
    ]
}

Response samples

Content type
application/json
[
  • {
    },
  • {
    },
  • {
    }
]

Delete a Machine/Instance Type association

Delete a Machine's association with an Instance Type.

@@ -7644,7 +7674,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

Instance

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/instance/type/{instanceTypeId}/machine/{machineAssociationId}

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

Instance

Instance is a Machine provisioned with an Operating System by a Tenant and attached to one or more VPC Prefixes or Subnets.

Retrieve all Instances

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
Example
[
  • {
    }
]

Create an Instance

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/instance

Response samples

Content type
application/json
Example
[
  • {
    }
]

Create an Instance

Create an Instance for Tenant.

Org must have a Tenant entity. User must have authorization role with TENANT_ADMIN suffix.

@@ -8222,7 +8252,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
Example
{
  • "name": "spark-monitor-1",
  • "description": "Node for monitoring Spark",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "instanceTypeId": "41e36058-8403-4086-a9b8-39cb5bc9cb98",
  • "vpcId": "5e28ad7c-5fb7-46d6-a28a-fc0ba6fdc4a3",
  • "operatingSystemId": "eaeb86ee-c435-444e-9e01-8346f67f194b",
  • "ipxeScript": "#!ipxe\nkernel http://archive.ubuntu.com/ubuntu/dists/xenial/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64/linux initrd=initrd.gz\ninitrd http://archive.ubuntu.com/ubuntu/dists/xenial/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64/initrd.gz\nboot || imgfree\nshell",
  • "alwaysBootWithCustomIpxe": true,
  • "userData": "#cloud-config\nautoinstall:\n apt:\n geoip: true\n preserve_sources_list: false\n primary:\n - arches: [amd64, i386]\n uri: http://archive.ubuntu.com/ubuntu\n - arches: [default]\n uri: http://ports.ubuntu.com/ubuntu-ports",
  • "labels": {
    },
  • "interfaces": [
    ],
  • "infinibandInterfaces": [
    ],
  • "nvLinkInterfaces": [
    ],
  • "sshKeyGroupIds": [
    ]
}

Response samples

Content type
application/json
Example
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "spark-monitor-1",
  • "description": "Node for monitoring Spark",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "instanceTypeId": "41e36058-8403-4086-a9b8-39cb5bc9cb98",
  • "vpcId": "5e28ad7c-5fb7-46d6-a28a-fc0ba6fdc4a3",
  • "machineId": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
  • "operatingSystemId": "eaeb86ee-c435-444e-9e01-8346f67f194b",
  • "controllerInstanceId": null,
  • "ipxeScript": "#!ipxe\nkernel http://archive.ubuntu.com/ubuntu/dists/xenial/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64/linux initrd=initrd.gz\ninitrd http://archive.ubuntu.com/ubuntu/dists/xenial/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64/initrd.gz\nboot || imgfree\nshell",
  • "alwaysBootWithCustomIpxe": true,
  • "userData": "#cloud-config\nautoinstall:\n apt:\n geoip: true\n preserve_sources_list: false\n primary:\n - arches: [amd64, i386]\n uri: http://archive.ubuntu.com/ubuntu\n - arches: [default]\n uri: http://ports.ubuntu.com/ubuntu-ports",
  • "labels": {
    },
  • "isUpdatePending": false,
  • "serialConsoleUrl": "ssh://user@nico.acme.com",
  • "interfaces": [
    ],
  • "infinibandInterfaces": [
    ],
  • "nvLinkInterfaces": [
    ],
  • "dpuExtensionServiceDeployments": [ ],
  • "sshKeyGroupIds": [
    ],
  • "sshKeyGroups": [
    ],
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Batch Create Instances

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/instance

Request samples

Content type
application/json
Example
{
  • "name": "spark-monitor-1",
  • "description": "Node for monitoring Spark",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "instanceTypeId": "41e36058-8403-4086-a9b8-39cb5bc9cb98",
  • "vpcId": "5e28ad7c-5fb7-46d6-a28a-fc0ba6fdc4a3",
  • "operatingSystemId": "eaeb86ee-c435-444e-9e01-8346f67f194b",
  • "ipxeScript": "#!ipxe\nkernel http://archive.ubuntu.com/ubuntu/dists/xenial/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64/linux initrd=initrd.gz\ninitrd http://archive.ubuntu.com/ubuntu/dists/xenial/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64/initrd.gz\nboot || imgfree\nshell",
  • "alwaysBootWithCustomIpxe": true,
  • "userData": "#cloud-config\nautoinstall:\n apt:\n geoip: true\n preserve_sources_list: false\n primary:\n - arches: [amd64, i386]\n uri: http://archive.ubuntu.com/ubuntu\n - arches: [default]\n uri: http://ports.ubuntu.com/ubuntu-ports",
  • "labels": {
    },
  • "interfaces": [
    ],
  • "infinibandInterfaces": [
    ],
  • "nvLinkInterfaces": [
    ],
  • "sshKeyGroupIds": [
    ]
}

Response samples

Content type
application/json
Example
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "spark-monitor-1",
  • "description": "Node for monitoring Spark",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "instanceTypeId": "41e36058-8403-4086-a9b8-39cb5bc9cb98",
  • "vpcId": "5e28ad7c-5fb7-46d6-a28a-fc0ba6fdc4a3",
  • "machineId": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
  • "operatingSystemId": "eaeb86ee-c435-444e-9e01-8346f67f194b",
  • "controllerInstanceId": null,
  • "ipxeScript": "#!ipxe\nkernel http://archive.ubuntu.com/ubuntu/dists/xenial/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64/linux initrd=initrd.gz\ninitrd http://archive.ubuntu.com/ubuntu/dists/xenial/main/installer-amd64/current/images/netboot/ubuntu-installer/amd64/initrd.gz\nboot || imgfree\nshell",
  • "alwaysBootWithCustomIpxe": true,
  • "userData": "#cloud-config\nautoinstall:\n apt:\n geoip: true\n preserve_sources_list: false\n primary:\n - arches: [amd64, i386]\n uri: http://archive.ubuntu.com/ubuntu\n - arches: [default]\n uri: http://ports.ubuntu.com/ubuntu-ports",
  • "labels": {
    },
  • "isUpdatePending": false,
  • "serialConsoleUrl": "ssh://user@nico.acme.com",
  • "interfaces": [
    ],
  • "infinibandInterfaces": [
    ],
  • "nvLinkInterfaces": [
    ],
  • "dpuExtensionServiceDeployments": [ ],
  • "sshKeyGroupIds": [
    ],
  • "sshKeyGroups": [
    ],
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Batch Create Instances

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
Example
{
  • "namePrefix": "gpu-worker",
  • "count": 4,
  • "description": "GPU worker nodes for distributed training",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "instanceTypeId": "41e36058-8403-4086-a9b8-39cb5bc9cb98",
  • "vpcId": "5e28ad7c-5fb7-46d6-a28a-fc0ba6fdc4a3",
  • "operatingSystemId": "eaeb86ee-c435-444e-9e01-8346f67f194b",
  • "topologyOptimized": true,
  • "interfaces": [
    ],
  • "infinibandInterfaces": [
    ],
  • "nvLinkInterfaces": [
    ],
  • "sshKeyGroupIds": [
    ]
}

Response samples

Content type
application/json
[
  • {
    },
  • {
    }
]

Retrieve Instance

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/instance/batch

Request samples

Content type
application/json
Example
{
  • "namePrefix": "gpu-worker",
  • "count": 4,
  • "description": "GPU worker nodes for distributed training",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "instanceTypeId": "41e36058-8403-4086-a9b8-39cb5bc9cb98",
  • "vpcId": "5e28ad7c-5fb7-46d6-a28a-fc0ba6fdc4a3",
  • "operatingSystemId": "eaeb86ee-c435-444e-9e01-8346f67f194b",
  • "topologyOptimized": true,
  • "interfaces": [
    ],
  • "infinibandInterfaces": [
    ],
  • "nvLinkInterfaces": [
    ],
  • "sshKeyGroupIds": [
    ]
}

Response samples

Content type
application/json
[
  • {
    },
  • {
    }
]

Retrieve Instance

Get an Instance by ID

Org must have a Tenant entity. Instance must belong to Tenant. User must have authorization role with TENANT_ADMIN suffix.

@@ -8778,7 +8808,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "spark-monitor-1",
  • "description": "Node for monitoring Spark",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "instanceTypeId": "41e36058-8403-4086-a9b8-39cb5bc9cb98",
  • "vpcId": "5e28ad7c-5fb7-46d6-a28a-fc0ba6fdc4a3",
  • "machineId": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
  • "operatingSystemId": "eaeb86ee-c435-444e-9e01-8346f67f194b",
  • "controllerInstanceId": null,
  • "ipxeScript": null,
  • "alwaysBootWithCustomIpxe": false,
  • "userData": null,
  • "networkSecurityGroupId": "c602eb90-3039-11f0-997a-b38d4fc8389e",
  • "networkSecurityGroupPropagationDetails": {
    },
  • "networkSecurityGroupInherited": false,
  • "labels": {
    },
  • "isUpdatePending": false,
  • "serialConsoleUrl": "ssh://user@nico.acme.com",
  • "interfaces": [
    ],
  • "infinibandInterfaces": [
    ],
  • "nvLinkInterfaces": [
    ],
  • "dpuExtensionServiceDeployments": [
    ],
  • "sshKeyGroupIds": [
    ],
  • "sshKeyGroups": [
    ],
  • "tpmEkCertificate": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMxVENDQWJ5Z0F3SUJBZ0lVTEE1ZHFPK1E5OXZQM3VYRTRKcjBncVRtOW93d0RRWUpLb1pJaHZjTkFRRUwKQlFBd0xqRUxNQWtHQTFVRUJoTUNWVk14RXpBUkJnTlZCQW9NQ2s1MmFXUnBZU0JEYjNKNw==",
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete Instance

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/instance/{instanceId}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "spark-monitor-1",
  • "description": "Node for monitoring Spark",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "instanceTypeId": "41e36058-8403-4086-a9b8-39cb5bc9cb98",
  • "vpcId": "5e28ad7c-5fb7-46d6-a28a-fc0ba6fdc4a3",
  • "machineId": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
  • "operatingSystemId": "eaeb86ee-c435-444e-9e01-8346f67f194b",
  • "controllerInstanceId": null,
  • "ipxeScript": null,
  • "alwaysBootWithCustomIpxe": false,
  • "userData": null,
  • "networkSecurityGroupId": "c602eb90-3039-11f0-997a-b38d4fc8389e",
  • "networkSecurityGroupPropagationDetails": {
    },
  • "networkSecurityGroupInherited": false,
  • "labels": {
    },
  • "isUpdatePending": false,
  • "serialConsoleUrl": "ssh://user@nico.acme.com",
  • "interfaces": [
    ],
  • "infinibandInterfaces": [
    ],
  • "nvLinkInterfaces": [
    ],
  • "dpuExtensionServiceDeployments": [
    ],
  • "sshKeyGroupIds": [
    ],
  • "sshKeyGroups": [
    ],
  • "tpmEkCertificate": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMxVENDQWJ5Z0F3SUJBZ0lVTEE1ZHFPK1E5OXZQM3VYRTRKcjBncVRtOW93d0RRWUpLb1pJaHZjTkFRRUwKQlFBd0xqRUxNQWtHQTFVRUJoTUNWVk14RXpBUkJnTlZCQW9NQ2s1MmFXUnBZU0JEYjNKNw==",
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete Instance

Delete an Instance by ID

Org must have a Tenant entity. Instance must belong to Tenant. User must have authorization role with TENANT_ADMIN suffix.

@@ -8804,7 +8834,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
{
  • "machineHealthIssue": {
    },
  • "isRepairTenant": false
}

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

Update Instance

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/instance/{instanceId}

Request samples

Content type
application/json
{
  • "machineHealthIssue": {
    },
  • "isRepairTenant": false
}

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

Update Instance

Update an Instance by ID

Org must have a Tenant entity. Instance must belong to Tenant. User must have authorization role with TENANT_ADMIN suffix.

@@ -9120,7 +9150,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
{
  • "name": "spark-monitor-1",
  • "description": "Spark Monitor Node 1",
  • "triggerReboot": true,
  • "rebootWithCustomIpxe": true,
  • "applyUpdatesOnReboot": true,
  • "sshKeyGroupIds": [
    ],
  • "labels": {
    },
  • "interfaces": [
    ],
  • "infinibandInterfaces": [
    ],
  • "nvLinkInterfaces": [
    ],
  • "dpuExtensionServiceDeployments": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "spark-monitor-2",
  • "description": "Spark Monitor Node 1",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "instanceTypeId": "41e36058-8403-4086-a9b8-39cb5bc9cb98",
  • "vpcId": "5e28ad7c-5fb7-46d6-a28a-fc0ba6fdc4a3",
  • "machineId": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
  • "operatingSystemId": "eaeb86ee-c435-444e-9e01-8346f67f194b",
  • "controllerInstanceId": "158fc2bc-f2fb-4e1f-a5a4-2211062d14df",
  • "ipxeScript": null,
  • "alwaysBootWithCustomIpxe": false,
  • "userData": null,
  • "labels": {
    },
  • "isUpdatePending": false,
  • "serialConsoleUrl": "ssh://user@nico.acme.com",
  • "interfaces": [
    ],
  • "infinibandInterfaces": [
    ],
  • "dpuExtensionServiceDeployments": [
    ],
  • "sshKeyGroupIds": [
    ],
  • "sshKeyGroups": [
    ],
  • "tpmEkCertificate": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMxVENDQWJ5Z0F3SUJBZ0lVTEE1ZHFPK1E5OXZQM3VYRTRKcjBncVRtOW93d0RRWUpLb1pJaHZjTkFRRUwKQlFBd0xqRUxNQWtHQTFVRUJoTUNWVk14RXpBUkJnTlZCQW9NQ2s1MmFXUnBZU0JEYjNKNw==",
  • "status": "Rebooting",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve Instance status history

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/instance/{instanceId}

Request samples

Content type
application/json
{
  • "name": "spark-monitor-1",
  • "description": "Spark Monitor Node 1",
  • "triggerReboot": true,
  • "rebootWithCustomIpxe": true,
  • "applyUpdatesOnReboot": true,
  • "sshKeyGroupIds": [
    ],
  • "labels": {
    },
  • "interfaces": [
    ],
  • "infinibandInterfaces": [
    ],
  • "nvLinkInterfaces": [
    ],
  • "dpuExtensionServiceDeployments": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "spark-monitor-2",
  • "description": "Spark Monitor Node 1",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "instanceTypeId": "41e36058-8403-4086-a9b8-39cb5bc9cb98",
  • "vpcId": "5e28ad7c-5fb7-46d6-a28a-fc0ba6fdc4a3",
  • "machineId": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
  • "operatingSystemId": "eaeb86ee-c435-444e-9e01-8346f67f194b",
  • "controllerInstanceId": "158fc2bc-f2fb-4e1f-a5a4-2211062d14df",
  • "ipxeScript": null,
  • "alwaysBootWithCustomIpxe": false,
  • "userData": null,
  • "labels": {
    },
  • "isUpdatePending": false,
  • "serialConsoleUrl": "ssh://user@nico.acme.com",
  • "interfaces": [
    ],
  • "infinibandInterfaces": [
    ],
  • "dpuExtensionServiceDeployments": [
    ],
  • "sshKeyGroupIds": [
    ],
  • "sshKeyGroups": [
    ],
  • "tpmEkCertificate": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMxVENDQWJ5Z0F3SUJBZ0lVTEE1ZHFPK1E5OXZQM3VYRTRKcjBncVRtOW93d0RRWUpLb1pJaHZjTkFRRUwKQlFBd0xqRUxNQWtHQTFVRUJoTUNWVk14RXpBUkJnTlZCQW9NQ2s1MmFXUnBZU0JEYjNKNw==",
  • "status": "Rebooting",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve Instance status history

Get Instance status history

Org must have a Tenant entity. User must have authorization role with TENANT_ADMIN suffix.

@@ -9150,7 +9180,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve all Interfaces

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/instance/{instanceId}/status-history

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve all Interfaces

Get all Interfaces for an Instance

Org must have a Tenant entity. User must have authorization role with TENANT_ADMIN suffix.

@@ -9208,7 +9238,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
[
  • {
    },
  • {
    }
]

Retrieve all Instance InfiniBand Interfaces

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/instance/{instanceId}/interface

Response samples

Content type
application/json
[
  • {
    },
  • {
    }
]

Retrieve all Instance InfiniBand Interfaces

Get all InfiniBand Interfaces for an Instance

Org must have a Tenant entity. User must have authorization role with TENANT_ADMIN suffix.

@@ -9258,7 +9288,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
[
  • {
    },
  • {
    }
]

Machine

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/instance/{instanceId}/nvlink-interface

Response samples

Content type
application/json
[
  • {
    },
  • {
    }
]

Machine

Machine is a physical server that contains CPUs, GPUs, memory, storage, and networking hardware. Machines are the physical building blocks of a Site.

Retrieve all Machines

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve a Machine

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/machine

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve a Machine

Org must have either an Infrastructure Provider entity or a Tenant entity.

@@ -9724,7 +9754,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
Example
{
  • "id": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "instanceTypeId": "2e016c02-2c67-48aa-b289-5d3ca6320c52",
  • "instanceId": "59bdaaff-3998-4fd9-a140-8749beeb605e",
  • "tenantId": "99819e6e-4017-4021-9edd-ea1bdf4dbd59",
  • "controllerMachineId": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
  • "controllerMachineType": "x86_64",
  • "hwSkuDeviceType": "cpu",
  • "vendor": "Lenovo",
  • "productName": "ThinkSystem SR670 V2",
  • "serialNumber": "J1060ACR.D3KS2CS001G",
  • "machineCapabilities": [
    ],
  • "machineInterfaces": [
    ],
  • "maintenanceMessage": null,
  • "health": {
    },
  • "labels": {
    },
  • "status": "Ready",
  • "isUsableByTenant": true,
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Update Machine

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/machine/{machineId}

Response samples

Content type
application/json
Example
{
  • "id": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "instanceTypeId": "2e016c02-2c67-48aa-b289-5d3ca6320c52",
  • "instanceId": "59bdaaff-3998-4fd9-a140-8749beeb605e",
  • "tenantId": "99819e6e-4017-4021-9edd-ea1bdf4dbd59",
  • "controllerMachineId": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
  • "controllerMachineType": "x86_64",
  • "hwSkuDeviceType": "cpu",
  • "vendor": "Lenovo",
  • "productName": "ThinkSystem SR670 V2",
  • "serialNumber": "J1060ACR.D3KS2CS001G",
  • "machineCapabilities": [
    ],
  • "machineInterfaces": [
    ],
  • "maintenanceMessage": null,
  • "health": {
    },
  • "labels": {
    },
  • "status": "Ready",
  • "isUsableByTenant": true,
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Update Machine

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
Example
{
  • "instanceTypeId": "2e016c02-2c67-48aa-b289-5d3ca6320c52"
}

Response samples

Content type
application/json
{
  • "id": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "instanceTypeId": "2e016c02-2c67-48aa-b289-5d3ca6320c52",
  • "instanceId": "59bdaaff-3998-4fd9-a140-8749beeb605e",
  • "tenantId": "99819e6e-4017-4021-9edd-ea1bdf4dbd59",
  • "controllerMachineId": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
  • "controllerMachineType": "x86_64",
  • "hwSkuDeviceType": "cpu",
  • "vendor": "Lenovo",
  • "productName": "ThinkSystem SR670 V2",
  • "serialNumber": "J1060ACR.D3KS2CS001G",
  • "machineCapabilities": [
    ],
  • "machineInterfaces": [
    ],
  • "maintenanceMessage": null,
  • "health": {
    },
  • "labels": {
    },
  • "status": "Ready",
  • "isUsableByTenant": true,
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete a Machine from a Site

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/machine/{machineId}

Request samples

Content type
application/json
Example
{
  • "instanceTypeId": "2e016c02-2c67-48aa-b289-5d3ca6320c52"
}

Response samples

Content type
application/json
{
  • "id": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "instanceTypeId": "2e016c02-2c67-48aa-b289-5d3ca6320c52",
  • "instanceId": "59bdaaff-3998-4fd9-a140-8749beeb605e",
  • "tenantId": "99819e6e-4017-4021-9edd-ea1bdf4dbd59",
  • "controllerMachineId": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
  • "controllerMachineType": "x86_64",
  • "hwSkuDeviceType": "cpu",
  • "vendor": "Lenovo",
  • "productName": "ThinkSystem SR670 V2",
  • "serialNumber": "J1060ACR.D3KS2CS001G",
  • "machineCapabilities": [
    ],
  • "machineInterfaces": [
    ],
  • "maintenanceMessage": null,
  • "health": {
    },
  • "labels": {
    },
  • "status": "Ready",
  • "isUsableByTenant": true,
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete a Machine from a Site

Org must have an Infrastructure Provider entity. Machine must belong to the Provider. User must have authorization role with PROVIDER_ADMIN suffix. Machine must meet certain criteria to be eligible for deletion.

Authorizations:
JWTBearerToken
path Parameters
org
required
string

Name of the Org

@@ -10014,7 +10044,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Describes an error response for 500 Internal Server Error

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

Machine power control

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/machine/{machineId}

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

Machine power control

Execute power control actions for a specific Machine.

Org must have an Infrastructure Provider entity and own the Site that the Machine belongs to. User must have authorization role with PROVIDER_ADMIN suffix.

@@ -10038,7 +10068,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Response when the API handler encounters an unexpected error

Request samples

Content type
application/json
Example
{
  • "action": "On"
}

Response samples

Content type
application/json
{
  • "message": "Power control accepted"
}

Retrieve Machine status history

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/machine/{machineId}/power

Request samples

Content type
application/json
Example
{
  • "action": "On"
}

Response samples

Content type
application/json
{
  • "message": "Power control accepted"
}

Retrieve Machine status history

Org must have either an Infrastructure Provider entity or a Tenant entity.

@@ -10070,7 +10100,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve DPU Machines attached to a host Machine

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/machine/{machineId}/status-history

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve DPU Machines attached to a host Machine

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Response when the API handler encounters an unexpected error

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve GPU stats for machines at a site

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/machine/{machineId}/dpu

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve GPU stats for machines at a site

Returns GPU summary stats grouped by GPU name for machines at the specified site.

User must have authorization role with PROVIDER_ADMIN suffix. The specified site must belong to the Provider.

@@ -10276,7 +10306,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve machine instance type assignment summary for a site

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/machine/gpu/stats

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve machine instance type assignment summary for a site

Returns machine counts grouped by assigned (has instance type) vs unassigned, broken down by status.

User must have authorization role with PROVIDER_ADMIN suffix. The specified site must belong to the Provider.

@@ -10322,7 +10352,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
{
  • "assigned": {
    },
  • "unassigned": {
    }
}

Retrieve detailed per-instance-type machine stats for a site

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/machine/instance-type/stats/summary

Response samples

Content type
application/json
{
  • "assigned": {
    },
  • "unassigned": {
    }
}

Retrieve detailed per-instance-type machine stats for a site

Returns machine stats for each instance type including allocation details and tenant breakdown.

User must have authorization role with PROVIDER_ADMIN suffix. The specified site must belong to the Provider.

@@ -10390,7 +10420,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve all Machine Capabilities

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/machine/instance-type/stats

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve all Machine Capabilities

Get all distinct Machine Capabilities across all Machines

Org must have an Infrastructure Provider entity. User must have authorization role with PROVIDER_ADMIN suffix.

@@ -10450,7 +10480,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
[
  • {
    },
  • {
    },
  • {
    },
  • {
    },
  • {
    },
  • {
    }
]

BMC Reset

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/machine-capability

Response samples

Content type
application/json
[
  • {
    },
  • {
    },
  • {
    },
  • {
    },
  • {
    },
  • {
    }
]

BMC Reset

BMC Reset allows resetting a Machine's BMC

Reset Machine BMC

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Response when the API handler encounters an unexpected error

Request samples

Content type
application/json
{
  • "useIpmiTool": true
}

Response samples

Content type
application/json
{
  • "message": "Machine BMC reset request was accepted"
}

DPU Reprovision

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/machine/{machineId}/bmc/reset

Request samples

Content type
application/json
{
  • "useIpmiTool": true
}

Response samples

Content type
application/json
{
  • "message": "Machine BMC reset request was accepted"
}

DPU Reprovision

DPU Reprovision allows re-provisioning Machine's DPUs

Reprovision Machine DPUs

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Response when the API handler encounters an unexpected error

Request samples

Content type
application/json
{
  • "mode": "Restart",
  • "updateFirmware": true
}

Response samples

Content type
application/json
{
  • "message": "DPU reprovisioning request was accepted"
}

Health Report

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/machine/{machineId}/dpu/reprovision

Request samples

Content type
application/json
{
  • "mode": "Restart",
  • "updateFirmware": true
}

Response samples

Content type
application/json
{
  • "message": "DPU reprovisioning request was accepted"
}

Health Report

Machine Health Report contains information about the health of a Machine including user enforced overrides

Retrieve all Machine health reports

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Response when the API handler encounters an unexpected error

Response samples

Content type
application/json
[
  • {
    }
]

Create or update Machine health report

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/machine/{machineId}/health-report

Response samples

Content type
application/json
[
  • {
    }
]

Create or update Machine health report

Add or update health report override for a specific Machine.

@@ -10632,7 +10662,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Response when the API handler encounters an unexpected error

Request samples

Content type
application/json
{
  • "source": "overrides.sre",
  • "mode": "Merge",
  • "alerts": [
    ]
}

Response samples

Content type
application/json
{
  • "source": "overrides.sre",
  • "triggeredBy": "operator",
  • "observedAt": "2026-06-24T12:00:00Z",
  • "mode": "Merge",
  • "alerts": [
    ]
}

Delete Machine health report

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/machine/{machineId}/health-report

Request samples

Content type
application/json
{
  • "source": "overrides.sre",
  • "mode": "Merge",
  • "alerts": [
    ]
}

Response samples

Content type
application/json
{
  • "source": "overrides.sre",
  • "triggeredBy": "operator",
  • "observedAt": "2026-06-24T12:00:00Z",
  • "mode": "Merge",
  • "alerts": [
    ]
}

Delete Machine health report

Remove a health report override for a specific Machine.

@@ -10656,7 +10686,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Response when the API handler encounters an unexpected error

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "Error validating request data",
  • "data": {
    }
}

Machine Capability

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/machine/{machineId}/health-report/{source}

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "Error validating request data",
  • "data": {
    }
}

Machine Capability

Machine Capability defines the hardware capabilities of a Machine. Machine Capabilities can be used to group Machines into Instance Types.

Rack

Rack is a physical enclosure that contains a number of Machines. Racks are the physical building blocks of a Site.

@@ -10746,7 +10776,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve a Rack

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/rack

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve a Rack

Get a Rack by ID.

Org must have an Infrastructure Provider entity. User must have authorization role with PROVIDER_ADMIN suffix.

@@ -10824,7 +10854,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when requested object is not found

Response samples

Content type
application/json
{
  • "id": "550e8400-e29b-41d4-a716-446655440000",
  • "name": "Rack-01",
  • "manufacturer": "Dell",
  • "model": "PowerEdge R750",
  • "serialNumber": "SN-RACK-001",
  • "description": "Primary compute rack",
  • "location": {
    },
  • "components": [
    ]
}

Validate Racks

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/rack/{id}

Response samples

Content type
application/json
{
  • "id": "550e8400-e29b-41d4-a716-446655440000",
  • "name": "Rack-01",
  • "manufacturer": "Dell",
  • "model": "PowerEdge R750",
  • "serialNumber": "SN-RACK-001",
  • "description": "Primary compute rack",
  • "location": {
    },
  • "components": [
    ]
}

Validate Racks

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
Example
{
  • "diffs": [ ],
  • "totalDiffs": 0,
  • "missingCount": 0,
  • "unexpectedCount": 0,
  • "mismatchCount": 0,
  • "matchCount": 10
}

Validate a Rack

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/rack/validation

Response samples

Content type
application/json
Example
{
  • "diffs": [ ],
  • "totalDiffs": 0,
  • "missingCount": 0,
  • "unexpectedCount": 0,
  • "mismatchCount": 0,
  • "matchCount": 10
}

Validate a Rack

Validate a Rack's components by comparing expected vs actual state.

@@ -10916,7 +10946,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
Example
{
  • "diffs": [ ],
  • "totalDiffs": 0,
  • "missingCount": 0,
  • "unexpectedCount": 0,
  • "mismatchCount": 0,
  • "matchCount": 5
}

Power control Racks

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/rack/{id}/validation

Response samples

Content type
application/json
Example
{
  • "diffs": [ ],
  • "totalDiffs": 0,
  • "missingCount": 0,
  • "unexpectedCount": 0,
  • "mismatchCount": 0,
  • "matchCount": 5
}

Power control Racks

Power control Racks with optional filters. If no filter is specified, targets all racks in the Site.

@@ -10956,7 +10986,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
Example
{
  • "siteId": "550e8400-e29b-41d4-a716-446655440000",
  • "state": "off"
}

Response samples

Content type
application/json
{
  • "taskIds": [
    ]
}

Power control a Rack

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/rack/power

Request samples

Content type
application/json
Example
{
  • "siteId": "550e8400-e29b-41d4-a716-446655440000",
  • "state": "off"
}

Response samples

Content type
application/json
{
  • "taskIds": [
    ]
}

Power control a Rack

Power control a Rack identified by Rack UUID.

@@ -11006,7 +11036,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
Example
{
  • "siteId": "550e8400-e29b-41d4-a716-446655440000",
  • "state": "on"
}

Response samples

Content type
application/json
{
  • "taskIds": [
    ]
}

Firmware update Racks

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/rack/{id}/power

Request samples

Content type
application/json
Example
{
  • "siteId": "550e8400-e29b-41d4-a716-446655440000",
  • "state": "on"
}

Response samples

Content type
application/json
{
  • "taskIds": [
    ]
}

Firmware update Racks

Update firmware on Racks with optional name filter. If no filter is specified, targets all racks in the Site.

Org must have an Infrastructure Provider entity. User must have authorization role with PROVIDER_ADMIN suffix.

@@ -11044,7 +11074,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
Example
{
  • "siteId": "550e8400-e29b-41d4-a716-446655440000"
}

Response samples

Content type
application/json
{
  • "taskIds": [
    ]
}

Firmware update a Rack

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/rack/firmware

Request samples

Content type
application/json
Example
{
  • "siteId": "550e8400-e29b-41d4-a716-446655440000"
}

Response samples

Content type
application/json
{
  • "taskIds": [
    ]
}

Firmware update a Rack

Update firmware on a Rack identified by Rack UUID.

Org must have an Infrastructure Provider entity. User must have authorization role with PROVIDER_ADMIN suffix.

@@ -11130,7 +11160,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
Example
{
  • "siteId": "550e8400-e29b-41d4-a716-446655440000",
  • "version": "24.11.0"
}

Response samples

Content type
application/json
{
  • "taskIds": [
    ]
}

Bring up Racks

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/rack/{id}/firmware

Request samples

Content type
application/json
Example
{
  • "siteId": "550e8400-e29b-41d4-a716-446655440000",
  • "version": "24.11.0"
}

Response samples

Content type
application/json
{
  • "taskIds": [
    ]
}

Bring up Racks

Bring up Racks with optional name filter. If no filter is specified, targets all racks in the Site.

Org must have an Infrastructure Provider entity. User must have authorization role with PROVIDER_ADMIN suffix.

@@ -11166,7 +11196,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
Example
{
  • "siteId": "550e8400-e29b-41d4-a716-446655440000"
}

Response samples

Content type
application/json
{
  • "taskIds": [
    ]
}

Bring up a Rack

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/rack/bringup

Request samples

Content type
application/json
Example
{
  • "siteId": "550e8400-e29b-41d4-a716-446655440000"
}

Response samples

Content type
application/json
{
  • "taskIds": [
    ]
}

Bring up a Rack

Bring up a Rack identified by Rack UUID.

Org must have an Infrastructure Provider entity. User must have authorization role with PROVIDER_ADMIN suffix.

@@ -11198,7 +11228,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
Example
{
  • "siteId": "550e8400-e29b-41d4-a716-446655440000"
}

Response samples

Content type
application/json
{
  • "taskIds": [
    ]
}

Retrieve all Tasks for a Rack

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/rack/{id}/bringup

Request samples

Content type
application/json
Example
{
  • "siteId": "550e8400-e29b-41d4-a716-446655440000"
}

Response samples

Content type
application/json
{
  • "taskIds": [
    ]
}

Retrieve all Tasks for a Rack

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when requested object is not found

Response samples

Content type
application/json
[
  • {
    }
]

Tray

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/rack/{id}/task

Response samples

Content type
application/json
[
  • {
    }
]

Tray

Tray represents a component within a Rack.

Retrieve all Trays

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve a Tray

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/tray

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve a Tray

Get a Tray by ID.

Org must have an Infrastructure Provider entity. User must have authorization role with PROVIDER_ADMIN suffix.

@@ -11416,7 +11446,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when requested object is not found

Response samples

Content type
application/json
{
  • "id": "660e8400-e29b-41d4-a716-446655440001",
  • "componentId": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
  • "type": "compute",
  • "name": "compute-tray-1",
  • "manufacturer": "NVIDIA",
  • "model": "GB200",
  • "serialNumber": "TSN001",
  • "description": "Compute tray in slot 1",
  • "firmwareVersion": "2.1.0",
  • "powerState": "on",
  • "position": {
    },
  • "rackId": "550e8400-e29b-41d4-a716-446655440000"
}

Validate Trays

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/tray/{id}

Response samples

Content type
application/json
{
  • "id": "660e8400-e29b-41d4-a716-446655440001",
  • "componentId": "fm100ht4v4mce2qstjnl8970nnj3ie6ecek4mtjn27pea4kre5gsa49jg0g",
  • "type": "compute",
  • "name": "compute-tray-1",
  • "manufacturer": "NVIDIA",
  • "model": "GB200",
  • "serialNumber": "TSN001",
  • "description": "Compute tray in slot 1",
  • "firmwareVersion": "2.1.0",
  • "powerState": "on",
  • "position": {
    },
  • "rackId": "550e8400-e29b-41d4-a716-446655440000"
}

Validate Trays

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
{
  • "diffs": [ ],
  • "totalDiffs": 0,
  • "missingCount": 0,
  • "unexpectedCount": 0,
  • "mismatchCount": 0,
  • "matchCount": 10
}

Validate a Tray

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/tray/validation

Response samples

Content type
application/json
{
  • "diffs": [ ],
  • "totalDiffs": 0,
  • "missingCount": 0,
  • "unexpectedCount": 0,
  • "mismatchCount": 0,
  • "matchCount": 10
}

Validate a Tray

Validate a Tray by comparing expected vs actual state.

@@ -11518,7 +11548,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
{
  • "diffs": [ ],
  • "totalDiffs": 0,
  • "missingCount": 0,
  • "unexpectedCount": 0,
  • "mismatchCount": 0,
  • "matchCount": 5
}

Power control Trays

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/tray/{id}/validation

Response samples

Content type
application/json
{
  • "diffs": [ ],
  • "totalDiffs": 0,
  • "missingCount": 0,
  • "unexpectedCount": 0,
  • "mismatchCount": 0,
  • "matchCount": 5
}

Power control Trays

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
Example
{
  • "siteId": "550e8400-e29b-41d4-a716-446655440000",
  • "state": "on"
}

Response samples

Content type
application/json
{
  • "taskIds": [
    ]
}

Power control a Tray

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/tray/power

Request samples

Content type
application/json
Example
{
  • "siteId": "550e8400-e29b-41d4-a716-446655440000",
  • "state": "on"
}

Response samples

Content type
application/json
{
  • "taskIds": [
    ]
}

Power control a Tray

Power control a Tray identified by Tray UUID.

@@ -11630,7 +11660,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
Example
{
  • "siteId": "550e8400-e29b-41d4-a716-446655440000",
  • "state": "on"
}

Response samples

Content type
application/json
{
  • "taskIds": [
    ]
}

Firmware update Trays

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/tray/{id}/power

Request samples

Content type
application/json
Example
{
  • "siteId": "550e8400-e29b-41d4-a716-446655440000",
  • "state": "on"
}

Response samples

Content type
application/json
{
  • "taskIds": [
    ]
}

Firmware update Trays

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
Example
{
  • "siteId": "550e8400-e29b-41d4-a716-446655440000"
}

Response samples

Content type
application/json
{
  • "taskIds": [
    ]
}

Firmware update a Tray

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/tray/firmware

Request samples

Content type
application/json
Example
{
  • "siteId": "550e8400-e29b-41d4-a716-446655440000"
}

Response samples

Content type
application/json
{
  • "taskIds": [
    ]
}

Firmware update a Tray

Update firmware on a Tray identified by Tray UUID.

Org must have an Infrastructure Provider entity. User must have authorization role with PROVIDER_ADMIN suffix.

@@ -11828,7 +11858,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
Example
{
  • "siteId": "550e8400-e29b-41d4-a716-446655440000",
  • "version": "24.11.0"
}

Response samples

Content type
application/json
{
  • "taskIds": [
    ]
}

Retrieve all Tasks for a Tray

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/tray/{id}/firmware

Request samples

Content type
application/json
Example
{
  • "siteId": "550e8400-e29b-41d4-a716-446655440000",
  • "version": "24.11.0"
}

Response samples

Content type
application/json
{
  • "taskIds": [
    ]
}

Retrieve all Tasks for a Tray

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when requested object is not found

Response samples

Content type
application/json
[
  • {
    }
]

Task

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/tray/{id}/task

Response samples

Content type
application/json
[
  • {
    }
]

Task

Task represents an asynchronous, site-scoped operation (for example firmware update, power state change, or rack bring-up). Tasks are created when operations run against Racks, Trays, or other components. Endpoints in this tag retrieve or cancel a Task by ID; list Tasks for a Rack or Tray under the Rack and Tray tags.

Retrieve a Task

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when requested object is not found

Response samples

Content type
application/json
{
  • "id": "550e8400-e29b-41d4-a716-446655440000",
  • "status": "Running",
  • "description": "Power on rack components",
  • "message": "Processing 3 of 5 components"
}

Cancel a Task

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/task/{id}

Response samples

Content type
application/json
{
  • "id": "550e8400-e29b-41d4-a716-446655440000",
  • "status": "Running",
  • "description": "Power on rack components",
  • "message": "Processing 3 of 5 components"
}

Cancel a Task

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when requested object is not found

Request samples

Content type
application/json
{
  • "siteId": "660e8400-e29b-41d4-a716-446655440000"
}

Response samples

Content type
application/json
{
  • "id": "550e8400-e29b-41d4-a716-446655440000",
  • "status": "Terminated",
  • "description": "Power on rack components",
  • "message": "Cancelled by user"
}

Rule

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/task/{id}/cancel

Request samples

Content type
application/json
{
  • "siteId": "660e8400-e29b-41d4-a716-446655440000"
}

Response samples

Content type
application/json
{
  • "id": "550e8400-e29b-41d4-a716-446655440000",
  • "status": "Terminated",
  • "description": "Power on rack components",
  • "message": "Cancelled by user"
}

Rule

Operation Rule defines, per Site, how a particular operation (for example PowerControl / power_on or FirmwareControl / upgrade) should be executed against a set of components: ordered execution stages, per-component-type concurrency, pre / main / post actions, timeouts, and retry policy. Rules are reusable templates owned by Flow; this tag exposes CRUD (POST, GET, PATCH, DELETE) over them.

Create an Operation Rule

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
{
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "name": "string",
  • "description": "string",
  • "operationType": "PowerControl",
  • "operationCode": "string",
  • "ruleDefinition": {
    }
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "string",
  • "description": "string",
  • "operationType": "PowerControl",
  • "operationCode": "string",
  • "ruleDefinition": {
    },
  • "isDefault": true,
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

List Operation Rules

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/task/rule

Request samples

Content type
application/json
{
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "name": "string",
  • "description": "string",
  • "operationType": "PowerControl",
  • "operationCode": "string",
  • "ruleDefinition": {
    }
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "string",
  • "description": "string",
  • "operationType": "PowerControl",
  • "operationCode": "string",
  • "ruleDefinition": {
    },
  • "isDefault": true,
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

List Operation Rules

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve an Operation Rule

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/task/rule

Response samples

Content type
application/json
[
  • {
    }
]

Retrieve an Operation Rule

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when requested object is not found

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "string",
  • "description": "string",
  • "operationType": "PowerControl",
  • "operationCode": "string",
  • "ruleDefinition": {
    },
  • "isDefault": true,
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Update an Operation Rule

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/task/rule/{id}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "string",
  • "description": "string",
  • "operationType": "PowerControl",
  • "operationCode": "string",
  • "ruleDefinition": {
    },
  • "isDefault": true,
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Update an Operation Rule

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when requested object is not found

Request samples

Content type
application/json
{
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "name": "string",
  • "description": "string",
  • "ruleDefinition": {
    }
}

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "Error validating request data",
  • "data": {
    }
}

Delete an Operation Rule

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/task/rule/{id}

Request samples

Content type
application/json
{
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "name": "string",
  • "description": "string",
  • "ruleDefinition": {
    }
}

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "Error validating request data",
  • "data": {
    }
}

Delete an Operation Rule

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when requested object is not found

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "Error validating request data",
  • "data": {
    }
}

Network Security Group

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/task/rule/{id}

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "Error validating request data",
  • "data": {
    }
}

Network Security Group

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Describes an error response for 501 Not Implemented

Response samples

Content type
application/json
[
  • {
    }
]

Create Network Security Group

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/network-security-group

Response samples

Content type
application/json
[
  • {
    }
]

Create Network Security Group

Create a Network Security Group for Tenant.

Org must have a Tenant entity. User must have authorization role with TENANT_ADMIN suffix.

@@ -12660,7 +12690,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Describes an error response for 501 Not Implemented

Request samples

Content type
application/json
{
  • "id": "2a21cf79-ea5e-4d28-b585-2e78948fcefb",
  • "name": "Spark VPC Firewall",
  • "description": "Security policies for machines in Spark VPC",
  • "siteId": "188a8f32-0001-45cf-b243-f62720a22cc4",
  • "rules": [
    ],
  • "labels": {
    }
}

Response samples

Content type
application/json
{
  • "id": "2a21cf79-ea5e-4d28-b585-2e78948fcefb",
  • "name": "Spark VPC Firewall",
  • "description": "Security policies for machines in Spark VPC",
  • "siteId": "56f1a3ed-3653-454f-b861-9136207be660",
  • "tenantId": "79595ebe-934f-4f19-bc74-c16aefd0c57a",
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "rules": [
    ],
  • "labels": {
    },
  • "created": "2025-02-26T18:17:44.861317-05:00",
  • "updated": "2025-02-26T18:17:44.861317-05:00"
}

Retrieve Network Security Group

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/network-security-group

Request samples

Content type
application/json
{
  • "id": "2a21cf79-ea5e-4d28-b585-2e78948fcefb",
  • "name": "Spark VPC Firewall",
  • "description": "Security policies for machines in Spark VPC",
  • "siteId": "188a8f32-0001-45cf-b243-f62720a22cc4",
  • "rules": [
    ],
  • "labels": {
    }
}

Response samples

Content type
application/json
{
  • "id": "2a21cf79-ea5e-4d28-b585-2e78948fcefb",
  • "name": "Spark VPC Firewall",
  • "description": "Security policies for machines in Spark VPC",
  • "siteId": "56f1a3ed-3653-454f-b861-9136207be660",
  • "tenantId": "79595ebe-934f-4f19-bc74-c16aefd0c57a",
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "rules": [
    ],
  • "labels": {
    },
  • "created": "2025-02-26T18:17:44.861317-05:00",
  • "updated": "2025-02-26T18:17:44.861317-05:00"
}

Retrieve Network Security Group

Get a Network Security Group by ID

Org must have a Tenant entity. Instance must belong to Tenant. User must have authorization role with TENANT_ADMIN suffix.

@@ -12746,7 +12776,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Describes an error response for 501 Not Implemented

Response samples

Content type
application/json
{
  • "id": "2a21cf79-ea5e-4d28-b585-2e78948fcefb",
  • "name": "Spark VPC Firewall",
  • "description": "Security policies for machines in Spark VPC",
  • "siteId": "56f1a3ed-3653-454f-b861-9136207be660",
  • "tenantId": "79595ebe-934f-4f19-bc74-c16aefd0c57a",
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "rules": [
    ],
  • "labels": {
    },
  • "created": "2025-02-26T18:17:44.861317-05:00",
  • "updated": "2025-02-26T18:17:44.861317-05:00"
}

Update Network Security Group

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/network-security-group/{networkSecurityGroupId}

Response samples

Content type
application/json
{
  • "id": "2a21cf79-ea5e-4d28-b585-2e78948fcefb",
  • "name": "Spark VPC Firewall",
  • "description": "Security policies for machines in Spark VPC",
  • "siteId": "56f1a3ed-3653-454f-b861-9136207be660",
  • "tenantId": "79595ebe-934f-4f19-bc74-c16aefd0c57a",
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "rules": [
    ],
  • "labels": {
    },
  • "created": "2025-02-26T18:17:44.861317-05:00",
  • "updated": "2025-02-26T18:17:44.861317-05:00"
}

Update Network Security Group

Update a Network Security Group by ID

@@ -12860,7 +12890,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Describes an error response for 501 Not Implemented

Request samples

Content type
application/json
{
  • "name": "Spark VPC Firewall",
  • "description": "Security policies for machines in Spark VPC",
  • "rules": [
    ],
  • "labels": {
    }
}

Response samples

Content type
application/json
{
  • "id": "2a21cf79-ea5e-4d28-b585-2e78948fcefb",
  • "name": "Spark VPC Firewall",
  • "description": "Security policies for machines in Spark VPC",
  • "siteId": "56f1a3ed-3653-454f-b861-9136207be660",
  • "tenantId": "79595ebe-934f-4f19-bc74-c16aefd0c57a",
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "rules": [
    ],
  • "labels": {
    },
  • "created": "2025-02-26T18:17:44.861317-05:00",
  • "updated": "2025-02-26T18:17:44.861317-05:00"
}

Delete Network Security Group

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/network-security-group/{networkSecurityGroupId}

Request samples

Content type
application/json
{
  • "name": "Spark VPC Firewall",
  • "description": "Security policies for machines in Spark VPC",
  • "rules": [
    ],
  • "labels": {
    }
}

Response samples

Content type
application/json
{
  • "id": "2a21cf79-ea5e-4d28-b585-2e78948fcefb",
  • "name": "Spark VPC Firewall",
  • "description": "Security policies for machines in Spark VPC",
  • "siteId": "56f1a3ed-3653-454f-b861-9136207be660",
  • "tenantId": "79595ebe-934f-4f19-bc74-c16aefd0c57a",
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "rules": [
    ],
  • "labels": {
    },
  • "created": "2025-02-26T18:17:44.861317-05:00",
  • "updated": "2025-02-26T18:17:44.861317-05:00"
}

Delete Network Security Group

Delete a Network Security Group by ID

@@ -12886,7 +12916,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Describes an error response for 501 Not Implemented

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

IP Block

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/network-security-group/{networkSecurityGroupId}

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

IP Block

IP Block is a contiguous block of IP addresses defined by a prefix and prefix length.

It can be used by the Provider to describe the overlay network of a particular Site. Providers can also use Allocations to delegate portions of these IP Blocks to Tenants.

@@ -12972,7 +13002,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
[
  • {
    }
]

Create IP Block

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/ipblock

Response samples

Content type
application/json
[
  • {
    }
]

Create IP Block

Create an IP block for the org.

@@ -13050,7 +13080,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
{
  • "name": "Public Network Overlay for Site SJC4",
  • "description": "This is the primary IP overlay for SJC4. All IPs are publicly routable",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "routingType": "Public",
  • "prefix": "202.168.1.0",
  • "prefixLength": 24,
  • "protocolVersion": "IPv4"
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "Public Network Overlay for Site SJC4",
  • "description": "This is the primary IP overlay for SJC4. All IPs are publicly routable",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "tenantId": null,
  • "routingType": "Public",
  • "prefix": "202.168.1.0",
  • "prefixLength": 24,
  • "protocolVersion": "IPv4",
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve IP Block

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/ipblock

Request samples

Content type
application/json
{
  • "name": "Public Network Overlay for Site SJC4",
  • "description": "This is the primary IP overlay for SJC4. All IPs are publicly routable",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "routingType": "Public",
  • "prefix": "202.168.1.0",
  • "prefixLength": 24,
  • "protocolVersion": "IPv4"
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "Public Network Overlay for Site SJC4",
  • "description": "This is the primary IP overlay for SJC4. All IPs are publicly routable",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "tenantId": null,
  • "routingType": "Public",
  • "prefix": "202.168.1.0",
  • "prefixLength": 24,
  • "protocolVersion": "IPv4",
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve IP Block

Retrieve an IP Block by ID.

User must have authorization role with PROVIDER_ADMIN or TENANT_ADMIN suffix.

@@ -13120,7 +13150,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "Public Network Overlay for Site SJC4",
  • "description": "This is the primary IP overlay for SJC4. All IPs are publicly routable",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "tenantId": null,
  • "routingType": "Public",
  • "prefix": "192.168.20.0",
  • "prefixLength": 24,
  • "protocolVersion": "IPv4",
  • "usageStats": {
    },
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete IP Block

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/ipblock/{ipBlockId}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "Public Network Overlay for Site SJC4",
  • "description": "This is the primary IP overlay for SJC4. All IPs are publicly routable",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "tenantId": null,
  • "routingType": "Public",
  • "prefix": "192.168.20.0",
  • "prefixLength": 24,
  • "protocolVersion": "IPv4",
  • "usageStats": {
    },
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete IP Block

Delete an IP block

@@ -13138,7 +13168,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

Update IP Block

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/ipblock/{ipBlockId}

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

Update IP Block

Update an existing IP Block

@@ -13208,7 +13238,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
{
  • "name": "Public Network Overlay for Site SJC-4",
  • "description": "This is the primary IP overlay for SJC-4. All IPs are publicly routable"
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "Public Network Overlay for Site SJC-4",
  • "description": "This is the primary IP overlay for SJC-4. All IPs are publicly routable",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "tenantId": null,
  • "routingType": "Public",
  • "prefix": "202.168.16.0",
  • "prefixLength": 20,
  • "protocolVersion": "IPv4",
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve All Derived IP Blocks

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/ipblock/{ipBlockId}

Request samples

Content type
application/json
{
  • "name": "Public Network Overlay for Site SJC-4",
  • "description": "This is the primary IP overlay for SJC-4. All IPs are publicly routable"
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "Public Network Overlay for Site SJC-4",
  • "description": "This is the primary IP overlay for SJC-4. All IPs are publicly routable",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "infrastructureProviderId": "e94bcfda-f6cb-42e4-80ec-516811e5abbf",
  • "tenantId": null,
  • "routingType": "Public",
  • "prefix": "202.168.16.0",
  • "prefixLength": 20,
  • "protocolVersion": "IPv4",
  • "status": "Pending",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve All Derived IP Blocks

Retrieve all child IP Blocks allocated to Tenants from a specific Provider super IP Block. When allocations are created from a super block, individual Tenant IP Blocks are created as a result.

@@ -13286,7 +13316,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
[
  • {
    }
]

DPU Extension Service

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/ipblock/{ipBlockId}/derived

Response samples

Content type
application/json
[
  • {
    }
]

DPU Extension Service

DPU Extension Service allows users to run custom services in the DPUs of their Instances. Currently K8s pods are the only supported service type.

Retrieve all DPU Extension Services

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
[
  • {
    }
]

Create DPU Extension Service

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/dpu-extension-service

Response samples

Content type
application/json
[
  • {
    }
]

Create DPU Extension Service

Create a DPU Extension Service for the current Tenant.

Org must have a Tenant entity. User must have authorization role with TENANT_ADMIN suffix.

@@ -13446,7 +13476,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
{
  • "name": "busybox",
  • "description": "Single, multi-call executable that contains stripped-down versions of common Unix utilities",
  • "serviceType": "KubernetesPod",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "data": "apiVersion: apps/v1\\nkind: Deployment\\nmetadata:\\n name: busybox-deployment\\n labels:\\n app: busybox\\nspec:\\n replicas: 1 # You can adjust the number of desired replicas here\\n selector:\\n matchLabels:\\n app: busybox\\n template:\\n metadata:\\n labels:\\n app: busybox\\n spec:\\n containers:\\n - name: busybox-container\\n image: busybox:latest # You can specify a different BusyBox image tag\\n command: [\"sh\", \"-c\", \"echo \\'BusyBox container running\\' && sleep 3600\"]",
  • "credentials": {},
  • "observability": {
    }
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "busybox",
  • "description": "Single, multi-call executable that contains stripped-down versions of common Unix utilities",
  • "serviceType": "KubernetesPod",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "version": "V1-T1761856992374052",
  • "versionInfo": {
    },
  • "activeVersions": [
    ],
  • "status": "Ready",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve DPU Extension Service

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/dpu-extension-service

Request samples

Content type
application/json
{
  • "name": "busybox",
  • "description": "Single, multi-call executable that contains stripped-down versions of common Unix utilities",
  • "serviceType": "KubernetesPod",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "data": "apiVersion: apps/v1\\nkind: Deployment\\nmetadata:\\n name: busybox-deployment\\n labels:\\n app: busybox\\nspec:\\n replicas: 1 # You can adjust the number of desired replicas here\\n selector:\\n matchLabels:\\n app: busybox\\n template:\\n metadata:\\n labels:\\n app: busybox\\n spec:\\n containers:\\n - name: busybox-container\\n image: busybox:latest # You can specify a different BusyBox image tag\\n command: [\"sh\", \"-c\", \"echo \\'BusyBox container running\\' && sleep 3600\"]",
  • "credentials": {},
  • "observability": {
    }
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "busybox",
  • "description": "Single, multi-call executable that contains stripped-down versions of common Unix utilities",
  • "serviceType": "KubernetesPod",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "version": "V1-T1761856992374052",
  • "versionInfo": {
    },
  • "activeVersions": [
    ],
  • "status": "Ready",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve DPU Extension Service

Retrieve a DPU Extension Service for the current Tenant by ID

@@ -13506,7 +13536,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "busybox",
  • "description": "Single, multi-call executable that contains stripped-down versions of common Unix utilities",
  • "serviceType": "KubernetesPod",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "version": "V1-T1761856992374052",
  • "versionInfo": {
    },
  • "activeVersions": [
    ],
  • "status": "Ready",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete DPU Extension Service

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/dpu-extension-service/{dpuExtensionServiceId}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "busybox",
  • "description": "Single, multi-call executable that contains stripped-down versions of common Unix utilities",
  • "serviceType": "KubernetesPod",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "version": "V1-T1761856992374052",
  • "versionInfo": {
    },
  • "activeVersions": [
    ],
  • "status": "Ready",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete DPU Extension Service

Delete a specific DPU Extension Service by ID. All versions will be deleted.

@@ -13522,7 +13552,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "User is not allowed to perform this action",
  • "data": null
}

Update DPU Extension Service

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/dpu-extension-service/{dpuExtensionServiceId}

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "User is not allowed to perform this action",
  • "data": null
}

Update DPU Extension Service

Update a specific DPU Extension Service.

@@ -13612,7 +13642,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when requested object is not found

Request samples

Content type
application/json
{
  • "name": "busybox-ha",
  • "data": "apiVersion: apps/v1\\nkind: Deployment\\nmetadata:\\n name: busybox-deployment\\n labels:\\n app: busybox\\nspec:\\n replicas: 3 # You can adjust the number of desired replicas here\\n selector:\\n matchLabels:\\n app: busybox\\n template:\\n metadata:\\n labels:\\n app: busybox\\n spec:\\n containers:\\n - name: busybox-container\\n image: busybox:latest # You can specify a different BusyBox image tag\\n command: [\"sh\", \"-c\", \"echo \\'BusyBox container running\\' && sleep 3600\"]",
  • "credentials": {},
  • "observability": {
    }
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "busybox",
  • "description": "Single, multi-call executable that contains stripped-down versions of common Unix utilities",
  • "serviceType": "KubernetesPod",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "version": "V1-T1761856992374052",
  • "versionInfo": {
    },
  • "activeVersions": [
    ],
  • "status": "Ready",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve DPU Extension Service Version

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/dpu-extension-service/{dpuExtensionServiceId}

Request samples

Content type
application/json
{
  • "name": "busybox-ha",
  • "data": "apiVersion: apps/v1\\nkind: Deployment\\nmetadata:\\n name: busybox-deployment\\n labels:\\n app: busybox\\nspec:\\n replicas: 3 # You can adjust the number of desired replicas here\\n selector:\\n matchLabels:\\n app: busybox\\n template:\\n metadata:\\n labels:\\n app: busybox\\n spec:\\n containers:\\n - name: busybox-container\\n image: busybox:latest # You can specify a different BusyBox image tag\\n command: [\"sh\", \"-c\", \"echo \\'BusyBox container running\\' && sleep 3600\"]",
  • "credentials": {},
  • "observability": {
    }
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "busybox",
  • "description": "Single, multi-call executable that contains stripped-down versions of common Unix utilities",
  • "serviceType": "KubernetesPod",
  • "siteId": "60189e9c-7d12-438c-b9ca-6998d9c364b1",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "version": "V1-T1761856992374052",
  • "versionInfo": {
    },
  • "activeVersions": [
    ],
  • "status": "Ready",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve DPU Extension Service Version

Retrieve details for a specific version of a DPU Extension Service.

@@ -13648,7 +13678,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when requested object is not found

Response samples

Content type
application/json
{
  • "version": "V1-T1761856992374052",
  • "data": "apiVersion: apps/v1\\nkind: Deployment\\nmetadata:\\n name: busybox-deployment\\n labels:\\n app: busybox\\nspec:\\n replicas: 1 # You can adjust the number of desired replicas here\\n selector:\\n matchLabels:\\n app: busybox\\n template:\\n metadata:\\n labels:\\n app: busybox\\n spec:\\n containers:\\n - name: busybox-container\\n image: busybox:latest # You can specify a different BusyBox image tag\\n command: [\"sh\", \"-c\", \"echo \\'BusyBox container running\\' && sleep 3600\"]",
  • "hasCredentials": true,
  • "created": "2019-08-24T14:15:22Z",
  • "observability": {
    }
}

Delete DPU Extension Service Version

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/dpu-extension-service/{dpuExtensionServiceId}/version/{version}

Response samples

Content type
application/json
{
  • "version": "V1-T1761856992374052",
  • "data": "apiVersion: apps/v1\\nkind: Deployment\\nmetadata:\\n name: busybox-deployment\\n labels:\\n app: busybox\\nspec:\\n replicas: 1 # You can adjust the number of desired replicas here\\n selector:\\n matchLabels:\\n app: busybox\\n template:\\n metadata:\\n labels:\\n app: busybox\\n spec:\\n containers:\\n - name: busybox-container\\n image: busybox:latest # You can specify a different BusyBox image tag\\n command: [\"sh\", \"-c\", \"echo \\'BusyBox container running\\' && sleep 3600\"]",
  • "hasCredentials": true,
  • "created": "2019-08-24T14:15:22Z",
  • "observability": {
    }
}

Delete DPU Extension Service Version

Delete a specific version of a DPU Extension Service.

@@ -13666,7 +13696,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "User is not allowed to perform this action",
  • "data": null
}

SSH Key Group

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/dpu-extension-service/{dpuExtensionServiceId}/version/{version}

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "User is not allowed to perform this action",
  • "data": null
}

SSH Key Group

SSH Key Groups allow grouping several SSH Keys together so they can be synced to Sites and used to access the Serial Console of Instances.

Retrieve all SSH Key Groups

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
[
  • {
    }
]

Create SSH Key Group

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/sshkeygroup

Response samples

Content type
application/json
[
  • {
    }
]

Create SSH Key Group

Create an SSH Key Group for the current Tenant.

Org must have a Tenant entity. User must have authorization role with TENANT_ADMIN suffix.

@@ -13828,7 +13858,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
{
  • "name": "reno-integration-sre",
  • "description": "SRE access SSH keys for Reno Integration",
  • "siteIds": [
    ],
  • "sshKeyIds": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "reno-integration-sre",
  • "description": "SRE access SSH keys for Reno Integration",
  • "org": "wdksahew1rqf",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "version": "fbc692b61ffef6fbfc38a3833f6b7e7ae508da75",
  • "siteAssociations": [
    ],
  • "sshKeys": [
    ],
  • "status": "Syncing",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve an SSH Key Group

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/sshkeygroup

Request samples

Content type
application/json
{
  • "name": "reno-integration-sre",
  • "description": "SRE access SSH keys for Reno Integration",
  • "siteIds": [
    ],
  • "sshKeyIds": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "reno-integration-sre",
  • "description": "SRE access SSH keys for Reno Integration",
  • "org": "wdksahew1rqf",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "version": "fbc692b61ffef6fbfc38a3833f6b7e7ae508da75",
  • "siteAssociations": [
    ],
  • "sshKeys": [
    ],
  • "status": "Syncing",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve an SSH Key Group

Retrieve an SSH Key Group for the current Tenant by ID

@@ -13898,7 +13928,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "reno-integration-sre",
  • "description": "SRE access SSH keys for Reno Integration",
  • "org": "wdksahew1rqf",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "version": "fbc692b61ffef6fbfc38a3833f6b7e7ae508da75",
  • "siteAssociations": [
    ],
  • "sshKeys": [
    ],
  • "status": "Syncing",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete an SSH Key Group

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/sshkeygroup/{sshKeyGroupId}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "reno-integration-sre",
  • "description": "SRE access SSH keys for Reno Integration",
  • "org": "wdksahew1rqf",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "version": "fbc692b61ffef6fbfc38a3833f6b7e7ae508da75",
  • "siteAssociations": [
    ],
  • "sshKeys": [
    ],
  • "status": "Syncing",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete an SSH Key Group

Delete a specific SSH key Group.

@@ -13914,7 +13944,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

Update an SSH Key Group

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/sshkeygroup/{sshKeyGroupId}

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

Update an SSH Key Group

Update a specific SSH Key Group.

@@ -13998,7 +14028,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
{
  • "name": "reno-int-sre",
  • "description": "SRE access SSH keys for Reno Integration Site",
  • "siteIds": [
    ],
  • "sshKeyIds": [
    ],
  • "version": "fbc692b61ffef6fbfc38a3833f6b7e7ae508da75"
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "reno-int-sre",
  • "description": "SRE access SSH keys for Reno Integration Site",
  • "org": "wdksahew1rqf",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "version": "fbc692b61ffef6fbfc38a3833f6b7e7ae508da75",
  • "siteAssociations": [
    ],
  • "sshKeys": [
    ],
  • "status": "Syncing",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

SSH Key

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/sshkeygroup/{sshKeyGroupId}

Request samples

Content type
application/json
{
  • "name": "reno-int-sre",
  • "description": "SRE access SSH keys for Reno Integration Site",
  • "siteIds": [
    ],
  • "sshKeyIds": [
    ],
  • "version": "fbc692b61ffef6fbfc38a3833f6b7e7ae508da75"
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "reno-int-sre",
  • "description": "SRE access SSH keys for Reno Integration Site",
  • "org": "wdksahew1rqf",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "version": "fbc692b61ffef6fbfc38a3833f6b7e7ae508da75",
  • "siteAssociations": [
    ],
  • "sshKeys": [
    ],
  • "status": "Syncing",
  • "statusHistory": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

SSH Key

SSH Key is a public key that can be used to access the Serial Console of an Instance.

Retrieve all SSH Keys

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
[
  • {
    }
]

Create SSH Key

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/sshkey

Response samples

Content type
application/json
[
  • {
    }
]

Create SSH Key

Create an SSH Key for the current Tenant. If an SSH Key Group is specified, all Sites associated with the SSH Key Group must be online.

Org must have a Tenant entity. User must have authorization role with TENANT_ADMIN suffix.

@@ -14070,7 +14100,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
{
  • "name": "reno-sre-access",
  • "publicKey": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICip4hl6WjuVHs60PeikVUs0sWE/kPhk2D0rRHWsIuyL jdoe@test.com",
  • "sshKeyGroupId": "86ca8cab-b285-4c2d-9e00-25c88810dc2e"
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "reno-sre-access",
  • "org": "xskkpgqpeakn",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "fingerprint": "CaK2yoj5fDOhf1swM2kFyjQrd3bwZfDYlWnVjBHgveQ",
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve an SSH key

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/sshkey

Request samples

Content type
application/json
{
  • "name": "reno-sre-access",
  • "publicKey": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICip4hl6WjuVHs60PeikVUs0sWE/kPhk2D0rRHWsIuyL jdoe@test.com",
  • "sshKeyGroupId": "86ca8cab-b285-4c2d-9e00-25c88810dc2e"
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "reno-sre-access",
  • "org": "xskkpgqpeakn",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "fingerprint": "CaK2yoj5fDOhf1swM2kFyjQrd3bwZfDYlWnVjBHgveQ",
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve an SSH key

Retrieve an SSH key for the current Tenant by ID

@@ -14098,7 +14128,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "staging-sre-access",
  • "org": "xskkpgqpeakn",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "fingerprint": "CaK2yoj5fDOhf1swM2kFyjQrd3bwZfDYlWnVjBHgveQ",
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete an SSH Key

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/sshkey/{sshKeyId}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "staging-sre-access",
  • "org": "xskkpgqpeakn",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "fingerprint": "CaK2yoj5fDOhf1swM2kFyjQrd3bwZfDYlWnVjBHgveQ",
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete an SSH Key

Delete an SSH key for the current Tenant by ID.

@@ -14114,7 +14144,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

Update an SSH Key

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/sshkey/{sshKeyId}

Response samples

Content type
application/json
{
  • "message": "Deletion request was accepted"
}

Update an SSH Key

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Request samples

Content type
application/json
{
  • "name": "reno-sre-access-v2"
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "reno-sre-access-v2",
  • "org": "xskkpgqpeakn",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "fingerprint": "CaK2yoj5fDOhf1swM2kFyjQrd3bwZfDYlWnVjBHgveQ",
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

User

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/sshkey/{sshKeyId}

Request samples

Content type
application/json
{
  • "name": "reno-sre-access-v2"
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "reno-sre-access-v2",
  • "org": "xskkpgqpeakn",
  • "tenantId": "f97df110-f4de-492e-8849-4a6af68026b0",
  • "fingerprint": "CaK2yoj5fDOhf1swM2kFyjQrd3bwZfDYlWnVjBHgveQ",
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

User

User is a logical entity that identifies individuals operating on behalf of an organization.

Retrieve Current User

Retrieve details of the current user.

@@ -14174,7 +14204,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authenticated

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "email": "janed@nvidia.com",
  • "firstName": "Jane",
  • "lastName": "Doe",
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Audit

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/user/current

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "email": "janed@nvidia.com",
  • "firstName": "Jane",
  • "lastName": "Doe",
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Audit

Audit is a record of actions taken by users on the API.

Retrieve all Audit Log Entries

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
[
  • {
    },
  • {
    }
]

Retrieve Audit Log Entry

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/audit

Response samples

Content type
application/json
[
  • {
    },
  • {
    }
]

Retrieve Audit Log Entry

Retrieve a specific Audit Log Entry by ID

User must have authorization role with PROVIDER_ADMIN or TENANT_ADMIN suffix

@@ -14294,7 +14324,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authorized to call an endpoint or retrieve/modify objects

Response samples

Content type
application/json
{
  • "id": "e313b3ca-c47a-4ec1-a79b-a147fad51a50",
  • "endpoint": "/v2/org/test-org-1/nico/ep",
  • "queryParams": "{\"test\":[\"1234\"]}",
  • "method": "POST",
  • "body": "{\"key1\":\"value1\"}",
  • "statusCode": 200,
  • "clientIP": "12.123.43.112",
  • "userID": "5d9fe319-14d4-40e3-8e5a-7d79e680d55b",
  • "user": {
    },
  • "orgName": "test-org-1",
  • "timestamp": "2024-12-04T21:06:33.849293-08:00",
  • "durationMs": 250,
  • "apiVersion": "0.1.91"
}

Metadata

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/audit/{auditEntryId}

Response samples

Content type
application/json
{
  • "id": "e313b3ca-c47a-4ec1-a79b-a147fad51a50",
  • "endpoint": "/v2/org/test-org-1/nico/ep",
  • "queryParams": "{\"test\":[\"1234\"]}",
  • "method": "POST",
  • "body": "{\"key1\":\"value1\"}",
  • "statusCode": 200,
  • "clientIP": "12.123.43.112",
  • "userID": "5d9fe319-14d4-40e3-8e5a-7d79e680d55b",
  • "user": {
    },
  • "orgName": "test-org-1",
  • "timestamp": "2024-12-04T21:06:33.849293-08:00",
  • "durationMs": 250,
  • "apiVersion": "0.1.91"
}

Metadata

Metadata describes various system-level attributes of the API service.

Retrieve metadata about the API server

Retrieve system metadata providing information about the API server

@@ -14310,7 +14340,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Error response when user is not authenticated

Response samples

Content type
application/json
{
  • "version": "0.1.24",
  • "buildTime": "2019-08-24T14:15:22Z"
}

Host Firmware Config

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/metadata

Response samples

Content type
application/json
{
  • "version": "0.1.24",
  • "buildTime": "2019-08-24T14:15:22Z"
}

Host Firmware Config

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Core gRPC API is unavailable, so the host firmware config request cannot be served.

Request samples

Content type
application/json
{
  • "siteId": "00000000-0000-0000-0000-000000000001",
  • "vendor": "Nvidia",
  • "model": "DGXH100",
  • "explicitStartNeeded": true,
  • "ordering": [
    ],
  • "components": [
    ]
}

Response samples

Content type
application/json
{
  • "vendor": "Nvidia",
  • "model": "DGXH100",
  • "explicitStartNeeded": true,
  • "ordering": [
    ],
  • "components": [
    ],
  • "created": "2025-08-24T14:15:22Z",
  • "updated": "2025-08-24T14:15:22Z"
}

Delete Host Firmware Config

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/firmware-config/host

Request samples

Content type
application/json
{
  • "siteId": "00000000-0000-0000-0000-000000000001",
  • "vendor": "Nvidia",
  • "model": "DGXH100",
  • "explicitStartNeeded": true,
  • "ordering": [
    ],
  • "components": [
    ]
}

Response samples

Content type
application/json
{
  • "vendor": "Nvidia",
  • "model": "DGXH100",
  • "explicitStartNeeded": true,
  • "ordering": [
    ],
  • "components": [
    ],
  • "created": "2025-08-24T14:15:22Z",
  • "updated": "2025-08-24T14:15:22Z"
}

Delete Host Firmware Config

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Core gRPC API is unavailable, so the host firmware config request cannot be served.

Request samples

Content type
application/json
{
  • "siteId": "00000000-0000-0000-0000-000000000001",
  • "vendor": "Nvidia",
  • "model": "DGXH100"
}

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "Error validating request data",
  • "data": {
    }
}

Tenant Identity

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/firmware-config/host

Request samples

Content type
application/json
{
  • "siteId": "00000000-0000-0000-0000-000000000001",
  • "vendor": "Nvidia",
  • "model": "DGXH100"
}

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "Error validating request data",
  • "data": {
    }
}

Tenant Identity

Typical API Call Flow for Tenant be served.

Request samples

Content type
application/json
Example
{
  • "enabled": true,
  • "issuer": "string",
  • "defaultAudience": "string",
  • "allowedAudiences": [ ],
  • "tokenTtlSeconds": 1,
  • "subjectPrefix": "string",
  • "rotateKey": false
}

Response samples

Content type
application/json
{
  • "org": "string",
  • "enabled": true,
  • "issuer": "string",
  • "defaultAudience": "string",
  • "allowedAudiences": [
    ],
  • "tokenTtlSeconds": 0,
  • "subjectPrefix": "string",
  • "signingKeys": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve Tenant Identity Configuration for current Org

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/site/{siteID}/tenant-identity/config

Request samples

Content type
application/json
Example
{
  • "enabled": true,
  • "issuer": "string",
  • "defaultAudience": "string",
  • "allowedAudiences": [ ],
  • "tokenTtlSeconds": 1,
  • "subjectPrefix": "string",
  • "rotateKey": false
}

Response samples

Content type
application/json
{
  • "org": "string",
  • "enabled": true,
  • "issuer": "string",
  • "defaultAudience": "string",
  • "allowedAudiences": [
    ],
  • "tokenTtlSeconds": 0,
  • "subjectPrefix": "string",
  • "signingKeys": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve Tenant Identity Configuration for current Org

Typical API Call Flow for Tenant be served.

Response samples

Content type
application/json
{
  • "org": "string",
  • "enabled": true,
  • "issuer": "string",
  • "defaultAudience": "string",
  • "allowedAudiences": [
    ],
  • "tokenTtlSeconds": 0,
  • "subjectPrefix": "string",
  • "signingKeys": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete Tenant Identity Configuration

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/site/{siteID}/tenant-identity/config

Response samples

Content type
application/json
{
  • "org": "string",
  • "enabled": true,
  • "issuer": "string",
  • "defaultAudience": "string",
  • "allowedAudiences": [
    ],
  • "tokenTtlSeconds": 0,
  • "subjectPrefix": "string",
  • "signingKeys": [
    ],
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete Tenant Identity Configuration

Typical API Call Flow for Tenant be served.

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "Error validating request data",
  • "data": {
    }
}

Create or Update Token Delegation

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/site/{siteID}/tenant-identity/config

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "Error validating request data",
  • "data": {
    }
}

Create or Update Token Delegation

Typical API Call Flow for Tenant be served.

Request samples

Content type
application/json
{
  • "tokenEndpoint": "http://example.com",
  • "clientSecretBasic": {
    },
  • "subjectTokenAudience": "string"
}

Response samples

Content type
application/json
{
  • "tokenEndpoint": "http://example.com",
  • "clientSecretBasic": {
    },
  • "subjectTokenAudience": "string",
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve Token Delegation for current Org

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/site/{siteID}/tenant-identity/token-delegation

Request samples

Content type
application/json
{
  • "tokenEndpoint": "http://example.com",
  • "clientSecretBasic": {
    },
  • "subjectTokenAudience": "string"
}

Response samples

Content type
application/json
{
  • "tokenEndpoint": "http://example.com",
  • "clientSecretBasic": {
    },
  • "subjectTokenAudience": "string",
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Retrieve Token Delegation for current Org

Retrieve the registered token exchange callback for the tenant.

@@ -15008,7 +15038,7 @@

Typical API Call Flow for Tenant

be served.

Response samples

Content type
application/json
{
  • "tokenEndpoint": "http://example.com",
  • "clientSecretBasic": {
    },
  • "subjectTokenAudience": "string",
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete Token Delegation

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/site/{siteID}/tenant-identity/token-delegation

Response samples

Content type
application/json
{
  • "tokenEndpoint": "http://example.com",
  • "clientSecretBasic": {
    },
  • "subjectTokenAudience": "string",
  • "created": "2019-08-24T14:15:22Z",
  • "updated": "2019-08-24T14:15:22Z"
}

Delete Token Delegation

Delete the RFC 8693 token exchange callback for the tenant.

@@ -15036,7 +15066,7 @@

Typical API Call Flow for Tenant

be served.

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "Error validating request data",
  • "data": {
    }
}

Retrieve OIDC JWKS for current Org

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/site/{siteID}/tenant-identity/token-delegation

Response samples

Content type
application/json
{
  • "source": "nico",
  • "message": "Error validating request data",
  • "data": {
    }
}

Retrieve OIDC JWKS for current Org

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Core gRPC API is unavailable.

Response samples

Content type
application/json
{
  • "keys": [
    ]
}

Retrieve OpenID Configuration for current Org

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/site/{siteID}/.well-known/jwks.json

Response samples

Content type
application/json
{
  • "keys": [
    ]
}

Retrieve OpenID Configuration for current Org

Typical API Call Flow for Tenant " class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Core gRPC API is unavailable.

Response samples

Content type
application/json
{
  • "issuer": "string",
  • "jwks_uri": "http://example.com",
  • "response_types_supported": [
    ],
  • "subject_types_supported": [
    ],
  • "id_token_signing_alg_values_supported": [
    ],
  • "spiffe_jwks_uri": "http://example.com"
}

Retrieve SPIFFE JWKS for current Org

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/site/{siteID}/.well-known/openid-configuration

Response samples

Content type
application/json
{
  • "issuer": "string",
  • "jwks_uri": "http://example.com",
  • "response_types_supported": [
    ],
  • "subject_types_supported": [
    ],
  • "id_token_signing_alg_values_supported": [
    ],
  • "spiffe_jwks_uri": "http://example.com"
}

Retrieve SPIFFE JWKS for current Org

SPIFFE trust-domain JWKS — same key material as the OIDC JWKS but with use: jwt-svid for SPIFFE-native verifiers. No authentication required.

Not-configured and malformed-body behavior matches the OIDC JWKS endpoint.

@@ -15152,7 +15182,7 @@

Typical API Call Flow for Tenant

" class="sc-iJSMbW sc-cBEgGa sc-ciCrSJ fiNpIH dNfUH dDDioG">

Core gRPC API is unavailable.

Response samples

Content type
application/json
{
  • "keys": [
    ]
}

Deprecations

https://nico-rest-api.nico.svc.cluster.local/v2/org/{org}/nico/site/{siteID}/.well-known/spiffe/jwks.json

Response samples

Content type
application/json
{
  • "keys": [
    ]
}

Deprecations

Typical API Call Flow for Tenant