@@ -20,13 +20,39 @@ use nexus_types::deployment::PlanningReport;
2020use nexus_types:: deployment:: { Blueprint , BlueprintTarget } ;
2121use nexus_types:: internal_api:: background:: BlueprintPlannerStatus ;
2222use nexus_types:: inventory:: Collection ;
23+ use omicron_common:: api:: external:: Error ;
2324use omicron_common:: api:: external:: LookupType ;
25+ use omicron_uuid_kinds:: BlueprintUuid ;
2426use omicron_uuid_kinds:: GenericUuid as _;
2527use serde_json:: json;
2628use slog_error_chain:: InlineErrorChain ;
2729use std:: sync:: Arc ;
2830use tokio:: sync:: watch:: { self , Receiver , Sender } ;
2931
32+ /// Error type for blueprint planning operations.
33+ #[ derive( Debug , thiserror:: Error ) ]
34+ enum PlanError {
35+ // Warning-level problems
36+ #[ error( "no target blueprint available" ) ]
37+ NoTargetBlueprint ,
38+ #[ error( "no inventory collection available" ) ]
39+ NoInventoryCollection ,
40+
41+ // Error-level problems
42+ #[ error( "failed to assemble planning input" ) ]
43+ AssemblePlanningInput ( #[ source] Error ) ,
44+ #[ error( "failed to make planner" ) ]
45+ MakePlanner ( #[ source] anyhow:: Error ) ,
46+ #[ error( "can't plan" ) ]
47+ Plan ( #[ source] nexus_reconfigurator_planning:: blueprint_builder:: Error ) ,
48+ #[ error( "can't save blueprint {blueprint_id}" ) ]
49+ SaveBlueprint {
50+ blueprint_id : BlueprintUuid ,
51+ #[ source]
52+ source : Error ,
53+ } ,
54+ }
55+
3056/// Background task that runs the update planner.
3157pub struct BlueprintPlanner {
3258 datastore : Arc < DataStore > ,
@@ -81,6 +107,39 @@ impl BlueprintPlanner {
81107 /// If it is different from the current target blueprint,
82108 /// save it and make it the current target.
83109 pub async fn plan ( & mut self , opctx : & OpContext ) -> BlueprintPlannerStatus {
110+ match self . plan_impl ( opctx) . await {
111+ Ok ( status) => status,
112+ Err ( plan_error) => {
113+ let error = InlineErrorChain :: new ( & plan_error) ;
114+ match & plan_error {
115+ PlanError :: NoTargetBlueprint
116+ | PlanError :: NoInventoryCollection => {
117+ warn ! (
118+ & opctx. log,
119+ "blueprint planning skipped" ;
120+ & error,
121+ ) ;
122+ }
123+ PlanError :: AssemblePlanningInput ( _)
124+ | PlanError :: MakePlanner { .. }
125+ | PlanError :: Plan ( _)
126+ | PlanError :: SaveBlueprint { .. } => {
127+ error ! (
128+ & opctx. log,
129+ "blueprint planning failed" ;
130+ & error,
131+ ) ;
132+ }
133+ }
134+ BlueprintPlannerStatus :: Error ( error. to_string ( ) )
135+ }
136+ }
137+ }
138+
139+ async fn plan_impl (
140+ & mut self ,
141+ opctx : & OpContext ,
142+ ) -> Result < BlueprintPlannerStatus , PlanError > {
84143 // Refuse to run if we haven't had a chance to load our config from the
85144 // database yet. (There might not be a config, which is fine! But the
86145 // loading task needs to have a chance to check.)
@@ -90,26 +149,19 @@ impl BlueprintPlanner {
90149 opctx. log,
91150 "reconfigurator config not yet loaded; doing nothing"
92151 ) ;
93- return BlueprintPlannerStatus :: Disabled ;
152+ return Ok ( BlueprintPlannerStatus :: Disabled ) ;
94153 }
95154 ReconfiguratorConfigLoaderState :: Loaded ( config) => config. clone ( ) ,
96155 } ;
97156 if !config. config . planner_enabled {
98157 debug ! ( & opctx. log, "blueprint planning disabled, doing nothing" ) ;
99- return BlueprintPlannerStatus :: Disabled ;
158+ return Ok ( BlueprintPlannerStatus :: Disabled ) ;
100159 }
101160
102161 // Get the current target blueprint to use as a parent.
103162 // Cloned so that we don't block the channel.
104163 let Some ( loaded) = self . rx_blueprint . borrow_and_update ( ) . clone ( ) else {
105- warn ! (
106- & opctx. log,
107- "blueprint planning skipped" ;
108- "reason" => "no target blueprint loaded"
109- ) ;
110- return BlueprintPlannerStatus :: Error ( String :: from (
111- "no target blueprint to use as parent for planning" ,
112- ) ) ;
164+ return Err ( PlanError :: NoTargetBlueprint ) ;
113165 } ;
114166 let ( target, parent) = & * loaded;
115167 let parent_blueprint_id = parent. id ;
@@ -120,69 +172,30 @@ impl BlueprintPlanner {
120172 let Some ( collection) =
121173 self . rx_inventory . borrow_and_update ( ) . as_ref ( ) . map ( Arc :: clone)
122174 else {
123- warn ! (
124- & opctx. log,
125- "blueprint planning skipped" ;
126- "reason" => "no inventory collection available"
127- ) ;
128- return BlueprintPlannerStatus :: Error ( String :: from (
129- "no inventory collection available" ,
130- ) ) ;
175+ return Err ( PlanError :: NoInventoryCollection ) ;
131176 } ;
132177
133178 // Assemble the planning context.
134- let input = match PlanningInputFromDb :: assemble (
179+ let input = PlanningInputFromDb :: assemble (
135180 opctx,
136181 & self . datastore ,
137182 config. config . planner_config ,
138183 )
139184 . await
140- {
141- Ok ( input) => input,
142- Err ( error) => {
143- error ! (
144- & opctx. log,
145- "can't assemble planning input" ;
146- "error" => %error,
147- ) ;
148- return BlueprintPlannerStatus :: Error ( format ! (
149- "can't assemble planning input: {error}"
150- ) ) ;
151- }
152- } ;
185+ . map_err ( PlanError :: AssemblePlanningInput ) ?;
153186
154187 // Generate a new blueprint.
155- let planner = match Planner :: new_based_on (
188+ let planner = Planner :: new_based_on (
156189 opctx. log . clone ( ) ,
157190 & parent,
158191 & input,
159192 "blueprint_planner" ,
160193 & collection,
161194 PlannerRng :: from_entropy ( ) ,
162- ) {
163- Ok ( planner) => planner,
164- Err ( error) => {
165- error ! (
166- & opctx. log,
167- "can't make planner" ;
168- "error" => %error,
169- "parent_blueprint_id" => %parent_blueprint_id,
170- ) ;
171- return BlueprintPlannerStatus :: Error ( format ! (
172- "can't make planner based on {}: {}" ,
173- parent_blueprint_id, error
174- ) ) ;
175- }
176- } ;
177- let blueprint = match planner. plan ( ) {
178- Ok ( blueprint) => blueprint,
179- Err ( error) => {
180- error ! ( & opctx. log, "can't plan: {error}" ) ;
181- return BlueprintPlannerStatus :: Error ( format ! (
182- "can't plan: {error}"
183- ) ) ;
184- }
185- } ;
195+ )
196+ . map_err ( PlanError :: MakePlanner ) ?;
197+
198+ let blueprint = planner. plan ( ) . map_err ( PlanError :: Plan ) ?;
186199
187200 // We just ran the planner, so we should always get its report. This
188201 // output is for debugging only, though, so just make an empty one in
@@ -216,7 +229,7 @@ impl BlueprintPlanner {
216229 match self . check_blueprint_limit_reached ( opctx, & report) . await {
217230 Ok ( count) => count,
218231 Err ( status) => {
219- return status;
232+ return Ok ( status) ;
220233 }
221234 } ;
222235
@@ -230,12 +243,12 @@ impl BlueprintPlanner {
230243 "blueprint unchanged from current target" ;
231244 "parent_blueprint_id" => %parent_blueprint_id,
232245 ) ;
233- return BlueprintPlannerStatus :: Unchanged {
246+ return Ok ( BlueprintPlannerStatus :: Unchanged {
234247 parent_blueprint_id,
235248 report,
236249 blueprint_count,
237250 limit : self . blueprint_limit ,
238- } ;
251+ } ) ;
239252 }
240253 }
241254
@@ -247,21 +260,9 @@ impl BlueprintPlanner {
247260 "parent_blueprint_id" => %parent_blueprint_id,
248261 "blueprint_id" => %blueprint_id,
249262 ) ;
250- match self . datastore . blueprint_insert ( opctx, & blueprint) . await {
251- Ok ( ( ) ) => ( ) ,
252- Err ( error) => {
253- error ! (
254- & opctx. log,
255- "can't save blueprint" ;
256- "error" => %error,
257- "blueprint_id" => %blueprint_id,
258- ) ;
259- return BlueprintPlannerStatus :: Error ( format ! (
260- "can't save blueprint {}: {}" ,
261- blueprint_id, error
262- ) ) ;
263- }
264- }
263+ self . datastore . blueprint_insert ( opctx, & blueprint) . await . map_err (
264+ |error| PlanError :: SaveBlueprint { blueprint_id, source : error } ,
265+ ) ?;
265266
266267 // Try to make it the current target.
267268 let target = BlueprintTarget {
@@ -299,27 +300,27 @@ impl BlueprintPlanner {
299300 ) ;
300301 }
301302 }
302- return BlueprintPlannerStatus :: Planned {
303+ return Ok ( BlueprintPlannerStatus :: Planned {
303304 parent_blueprint_id,
304305 error : format ! ( "{error}" ) ,
305306 report,
306307 blueprint_count,
307308 limit : self . blueprint_limit ,
308- } ;
309+ } ) ;
309310 }
310311 }
311312
312313 // We have a new target!
313314
314315 self . tx_blueprint . send_replace ( Some ( Arc :: new ( ( target, blueprint) ) ) ) ;
315- BlueprintPlannerStatus :: Targeted {
316+ Ok ( BlueprintPlannerStatus :: Targeted {
316317 parent_blueprint_id,
317318 blueprint_id,
318319 report,
319320 // A new blueprint was added, so increment the count by 1.
320321 blueprint_count : blueprint_count + 1 ,
321322 limit : self . blueprint_limit ,
322- }
323+ } )
323324 }
324325
325326 async fn check_blueprint_limit_reached (
0 commit comments