11//! Subnet management handlers
22
3+ use axum:: Json ;
34use axum:: extract:: State ;
45use axum:: http:: StatusCode ;
5- use axum:: Json ;
6- use serde:: Deserialize ;
6+ use serde:: { Deserialize , Serialize } ;
77use slog:: info;
88use utoipa:: ToSchema ;
99use 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 } ;
1512use 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}
0 commit comments