Skip to content

Commit 03d0547

Browse files
Add subnet handler
1 parent 12b50f9 commit 03d0547

7 files changed

Lines changed: 296 additions & 115 deletions

File tree

Cargo.lock

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rs/cloud-engine-controller-backend/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ ic-registry-nns-data-provider = { workspace = true }
3838
ic-registry-common-proto = { workspace = true }
3939
ic-registry-keys = { workspace = true }
4040
ic-protobuf = { workspace = true }
41-
prost = { workspace = true }
4241

4342
# GCP
4443
gcp_auth = { workspace = true }

rs/cloud-engine-controller-backend/src/handlers/subnet_handlers.rs

Lines changed: 133 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
//! Subnet management handlers
22
3+
use axum::Json;
34
use axum::extract::State;
45
use axum::http::StatusCode;
5-
use axum::Json;
6-
use serde::Deserialize;
6+
use serde::{Deserialize, Serialize};
77
use slog::info;
88
use utoipa::ToSchema;
99
use uuid::Uuid;
1010

11-
use crate::models::subnet::{
12-
ProposalStatus, SubnetListResponse, SubnetProposal,
13-
SubnetProposalResponse,
14-
};
11+
use crate::models::subnet::{ProposalStatus, SubnetListResponse, SubnetProposal, SubnetProposalResponse, SubnetUpgradeRequest};
1512
use crate::state::AppState;
1613

1714
/// Request to create a subnet proposal
@@ -54,16 +51,15 @@ pub struct DeleteSubnetRequest {
5451
(status = 500, description = "Internal server error")
5552
)
5653
)]
57-
pub async fn list_subnets(
58-
State(state): State<AppState>,
59-
) -> Result<Json<SubnetListResponse>, (StatusCode, String)> {
60-
let node_operator = state.config.node_operator.as_ref()
54+
pub async fn list_subnets(State(state): State<AppState>) -> Result<Json<SubnetListResponse>, (StatusCode, String)> {
55+
let node_operator = state
56+
.config
57+
.node_operator
58+
.as_ref()
6159
.ok_or((StatusCode::BAD_REQUEST, "Node operator not configured in config file".to_string()))?;
6260

6361
// Get subnets containing operator's nodes
64-
let subnets = state
65-
.registry_manager
66-
.get_subnets_by_operator(&node_operator.principal_id);
62+
let subnets = state.registry_manager.get_subnets_by_operator(&node_operator.principal_id);
6763

6864
let total = subnets.len();
6965
Ok(Json(SubnetListResponse { subnets, total }))
@@ -85,7 +81,10 @@ pub async fn create_subnet_proposal(
8581
State(state): State<AppState>,
8682
Json(request): Json<CreateSubnetProposalRequest>,
8783
) -> Result<Json<SubnetProposalResponse>, (StatusCode, String)> {
88-
let node_operator = state.config.node_operator.as_ref()
84+
let node_operator = state
85+
.config
86+
.node_operator
87+
.as_ref()
8988
.ok_or((StatusCode::BAD_REQUEST, "Node operator not configured in config file".to_string()))?;
9089

9190
// Validate that all nodes exist and belong to the configured operator
@@ -96,18 +95,15 @@ pub async fn create_subnet_proposal(
9695
.ok_or((StatusCode::NOT_FOUND, format!("Node {} not found", node_id)))?;
9796

9897
if node.operator_id.to_string() != node_operator.principal_id {
99-
return Err((StatusCode::BAD_REQUEST, format!(
100-
"Node {} does not belong to the configured node operator",
101-
node_id
102-
)));
98+
return Err((
99+
StatusCode::BAD_REQUEST,
100+
format!("Node {} does not belong to the configured node operator", node_id),
101+
));
103102
}
104103

105104
// Check if node is already assigned to a subnet
106105
if node.subnet_id.is_some() {
107-
return Err((StatusCode::BAD_REQUEST, format!(
108-
"Node {} is already assigned to a subnet",
109-
node_id
110-
)));
106+
return Err((StatusCode::BAD_REQUEST, format!("Node {} is already assigned to a subnet", node_id)));
111107
}
112108
}
113109

@@ -129,9 +125,7 @@ pub async fn create_subnet_proposal(
129125
};
130126

131127
// Store the proposal
132-
state
133-
.subnet_proposals
134-
.insert(proposal.id.clone(), proposal.clone());
128+
state.subnet_proposals.insert(proposal.id.clone(), proposal.clone());
135129

136130
info!(state.log, "Subnet creation proposal created";
137131
"proposal_id" => &proposal.id,
@@ -162,21 +156,22 @@ pub async fn delete_subnet_proposal(
162156
State(state): State<AppState>,
163157
Json(request): Json<DeleteSubnetRequest>,
164158
) -> Result<Json<SubnetProposalResponse>, (StatusCode, String)> {
165-
let node_operator = state.config.node_operator.as_ref()
159+
let node_operator = state
160+
.config
161+
.node_operator
162+
.as_ref()
166163
.ok_or((StatusCode::BAD_REQUEST, "Node operator not configured in config file".to_string()))?;
167164

168165
// Verify the subnet exists and has nodes from the configured operator
169-
let subnets = state
170-
.registry_manager
171-
.get_subnets_by_operator(&node_operator.principal_id);
166+
let subnets = state.registry_manager.get_subnets_by_operator(&node_operator.principal_id);
172167

173-
let subnet = subnets
174-
.iter()
175-
.find(|s| s.subnet_id == request.subnet_id)
176-
.ok_or((StatusCode::NOT_FOUND, format!(
168+
let subnet = subnets.iter().find(|s| s.subnet_id == request.subnet_id).ok_or((
169+
StatusCode::NOT_FOUND,
170+
format!(
177171
"Subnet {} not found or doesn't contain nodes from the configured operator",
178172
request.subnet_id
179-
)))?;
173+
),
174+
))?;
180175

181176
// Create deletion proposal
182177
let proposal = SubnetProposal {
@@ -190,9 +185,7 @@ pub async fn delete_subnet_proposal(
190185
created_at: chrono::Utc::now(),
191186
};
192187

193-
state
194-
.subnet_proposals
195-
.insert(proposal.id.clone(), proposal.clone());
188+
state.subnet_proposals.insert(proposal.id.clone(), proposal.clone());
196189

197190
info!(state.log, "Subnet deletion proposal created";
198191
"proposal_id" => &proposal.id,
@@ -203,9 +196,110 @@ pub async fn delete_subnet_proposal(
203196
id: proposal.id,
204197
proposal_id: None,
205198
status: ProposalStatus::Draft,
199+
message: format!("Deletion proposal for subnet {} created in draft state.", request.subnet_id),
200+
}))
201+
}
202+
203+
/// Response for subnet upgrade proposal
204+
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
205+
pub struct SubnetUpgradeResponse {
206+
/// Local tracking ID
207+
pub id: String,
208+
/// Subnet ID
209+
pub subnet_id: String,
210+
/// Current GuestOS version
211+
#[serde(skip_serializing_if = "Option::is_none")]
212+
pub current_version: Option<String>,
213+
/// Target GuestOS version
214+
pub target_version: String,
215+
/// Status
216+
pub status: ProposalStatus,
217+
/// Message
218+
pub message: String,
219+
}
220+
221+
/// Create a subnet upgrade proposal to update GuestOS version
222+
#[utoipa::path(
223+
post,
224+
path = "/subnets/upgrade",
225+
tag = "Subnets",
226+
request_body = SubnetUpgradeRequest,
227+
responses(
228+
(status = 200, description = "Upgrade proposal created", body = SubnetUpgradeResponse),
229+
(status = 400, description = "Invalid request"),
230+
(status = 404, description = "Subnet not found")
231+
)
232+
)]
233+
pub async fn upgrade_subnet(
234+
State(state): State<AppState>,
235+
Json(request): Json<SubnetUpgradeRequest>,
236+
) -> Result<Json<SubnetUpgradeResponse>, (StatusCode, String)> {
237+
// Get subnet info to validate it exists and get current version
238+
let subnet = state
239+
.registry_manager
240+
.get_subnet(&request.subnet_id)
241+
.ok_or((StatusCode::NOT_FOUND, format!("Subnet {} not found", request.subnet_id)))?;
242+
243+
let current_version = subnet.replica_version.clone();
244+
245+
// Check if already on target version
246+
if current_version.as_ref() == Some(&request.guestos_version_id) {
247+
return Err((
248+
StatusCode::BAD_REQUEST,
249+
format!(
250+
"Subnet {} is already running GuestOS version {}",
251+
request.subnet_id, request.guestos_version_id
252+
),
253+
));
254+
}
255+
256+
// Generate default title and summary if not provided
257+
let title = request
258+
.title
259+
.unwrap_or_else(|| format!("Update subnet {} to GuestOS version {}", request.subnet_id, request.guestos_version_id));
260+
261+
let summary = request.summary.unwrap_or_else(|| {
262+
format!(
263+
"Proposal to update subnet {} from GuestOS version {} to {}",
264+
request.subnet_id,
265+
current_version.as_deref().unwrap_or("unknown"),
266+
request.guestos_version_id
267+
)
268+
});
269+
270+
// Create upgrade proposal (in draft state)
271+
let proposal_id = Uuid::new_v4().to_string();
272+
273+
// Store as a subnet proposal with upgrade type
274+
let proposal = SubnetProposal {
275+
id: proposal_id.clone(),
276+
proposal_id: None,
277+
status: ProposalStatus::Draft,
278+
node_ids: subnet.node_ids.clone(),
279+
subnet_type: format!("upgrade:{}", request.guestos_version_id),
280+
title: title.clone(),
281+
summary: summary.clone(),
282+
created_at: chrono::Utc::now(),
283+
};
284+
285+
state.subnet_proposals.insert(proposal_id.clone(), proposal);
286+
287+
info!(state.log, "Subnet upgrade proposal created";
288+
"proposal_id" => &proposal_id,
289+
"subnet_id" => &request.subnet_id,
290+
"current_version" => current_version.as_deref().unwrap_or("unknown"),
291+
"target_version" => &request.guestos_version_id
292+
);
293+
294+
Ok(Json(SubnetUpgradeResponse {
295+
id: proposal_id,
296+
subnet_id: request.subnet_id,
297+
current_version,
298+
target_version: request.guestos_version_id,
299+
status: ProposalStatus::Draft,
206300
message: format!(
207-
"Deletion proposal for subnet {} created in draft state.",
208-
request.subnet_id
301+
"Upgrade proposal created in draft state. To submit to NNS, use: ic-admin propose-to-deploy-guestos-to-all-subnet-nodes --title \"{}\" --summary \"{}\"",
302+
title, summary
209303
),
210304
}))
211305
}

rs/cloud-engine-controller-backend/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ fn main() {
106106
.route("/subnets/list", get(subnet_handlers::list_subnets))
107107
.route("/subnets/create", post(subnet_handlers::create_subnet_proposal))
108108
.route("/subnets/delete", post(subnet_handlers::delete_subnet_proposal))
109+
.route("/subnets/upgrade", post(subnet_handlers::upgrade_subnet))
109110
// Swagger UI
110111
.merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi()))
111112
// Layers

rs/cloud-engine-controller-backend/src/models/subnet.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ pub struct SubnetInfo {
1111
/// Subnet type (application, system, etc.)
1212
#[serde(skip_serializing_if = "Option::is_none")]
1313
pub subnet_type: Option<String>,
14+
/// Current GuestOS replica version
15+
#[serde(skip_serializing_if = "Option::is_none")]
16+
pub replica_version: Option<String>,
1417
/// Node IDs in this subnet
1518
pub node_ids: Vec<String>,
1619
/// Number of nodes in the subnet
@@ -108,3 +111,18 @@ pub struct SubnetDeleteRequest {
108111
/// Proposal summary/motivation
109112
pub summary: String,
110113
}
114+
115+
/// Request to upgrade a subnet to a new GuestOS version
116+
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
117+
pub struct SubnetUpgradeRequest {
118+
/// Subnet ID to upgrade
119+
pub subnet_id: String,
120+
/// Target GuestOS version ID
121+
pub guestos_version_id: String,
122+
/// Proposal title (optional, will be auto-generated if not provided)
123+
#[serde(skip_serializing_if = "Option::is_none")]
124+
pub title: Option<String>,
125+
/// Proposal summary (optional, will be auto-generated if not provided)
126+
#[serde(skip_serializing_if = "Option::is_none")]
127+
pub summary: Option<String>,
128+
}

rs/cloud-engine-controller-backend/src/openapi.rs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,10 @@ use utoipa::OpenApi;
44

55
use crate::config::{AppConfig, GcpConfig, NodeOperatorConfig};
66
use crate::handlers::node_handlers::{GetNodeRequest, NodeInfo, NodeListResponse};
7-
use crate::handlers::subnet_handlers::{CreateSubnetProposalRequest, DeleteSubnetRequest};
7+
use crate::handlers::subnet_handlers::{CreateSubnetProposalRequest, DeleteSubnetRequest, SubnetUpgradeResponse};
88
use crate::handlers::vm_handlers::{DeleteVmRequest, ProvisionVmRequestBody};
9-
use crate::models::subnet::{
10-
ProposalStatus, SubnetInfo, SubnetListResponse, SubnetProposal,
11-
SubnetProposalResponse,
12-
};
13-
use crate::models::vm::{
14-
IcpNodeMapping, Vm, VmListResponse, VmProvisionRequest, VmProvisionResponse, VmStatus,
15-
};
9+
use crate::models::subnet::{ProposalStatus, SubnetInfo, SubnetListResponse, SubnetProposal, SubnetProposalResponse, SubnetUpgradeRequest};
10+
use crate::models::vm::{IcpNodeMapping, Vm, VmListResponse, VmProvisionRequest, VmProvisionResponse, VmStatus};
1611

1712
/// OpenAPI documentation
1813
#[derive(OpenApi)]
@@ -31,6 +26,7 @@ use crate::models::vm::{
3126
crate::handlers::subnet_handlers::list_subnets,
3227
crate::handlers::subnet_handlers::create_subnet_proposal,
3328
crate::handlers::subnet_handlers::delete_subnet_proposal,
29+
crate::handlers::subnet_handlers::upgrade_subnet,
3430
),
3531
info(
3632
title = "Cloud Engine Controller Backend",
@@ -72,6 +68,8 @@ use crate::models::vm::{
7268
SubnetListResponse,
7369
CreateSubnetProposalRequest,
7470
DeleteSubnetRequest,
71+
SubnetUpgradeRequest,
72+
SubnetUpgradeResponse,
7573
)
7674
),
7775
tags(

0 commit comments

Comments
 (0)