11import os
2- import re
32import sys
43from pathlib import Path
54from typing import Any , Dict , Optional
1110from mako .template import Template
1211
1312from .exceptions import ConfigurationError
13+ from .incus_cli import IncusCLI
14+ from .provisioners import REGISTERED_PROVISIONERS
1415from .reporter import Reporter
1516from .types import InstanceConfig , InstanceDict
1617
1718
1819class ConfigManager :
1920 def __init__ (
2021 self ,
22+ incus : IncusCLI ,
2123 reporter : Reporter ,
2224 config_path : Optional [str ] = None ,
2325 verbose : bool = False ,
2426 no_config : bool = False ,
2527 ):
28+ self .incus = incus
2629 self .reporter = reporter
2730 self .config_path = config_path
2831 self .verbose = verbose
@@ -130,13 +133,14 @@ def dump_config(self):
130133 except Exception as e : # pylint: disable=broad-exception-caught
131134 raise ConfigurationError (f"Error dumping configuration: { e } " ) from e
132135
133- def _validate_provision_step (self , step , step_idx , name ) :
136+ def _validate_provision_step (self , step : Any , step_idx : int , name : str ) -> None :
134137 if isinstance (step , str ):
138+ REGISTERED_PROVISIONERS .get ("script" )(self .incus , self .reporter ).validate_config (name , step )
135139 return
136140
137141 if not isinstance (step , dict ):
138142 raise ConfigurationError (
139- f"Provisioning step { step_idx } in instance '{ name } ' " " must be a string or a dictionary."
143+ f"Provisioning step { step_idx } in instance '{ name } ' must be a string or a dictionary."
140144 )
141145
142146 if len (step ) != 1 :
@@ -147,102 +151,32 @@ def _validate_provision_step(self, step, step_idx, name):
147151
148152 key , value = list (step .items ())[0 ]
149153
150- if key not in [ "copy" , "ssh" , "llmnr" ] :
154+ if key not in REGISTERED_PROVISIONERS . keys () :
151155 raise ConfigurationError (
152156 f"Unknown provisioning step type '{ key } ' in instance '{ name } '. "
153- "Accepted types are 'copy' , 'ssh', or 'llmnr' ."
157+ f "Accepted types are { ' , '. join ( REGISTERED_PROVISIONERS . keys ()) } ."
154158 )
155159
156- if key == "copy" :
157- if not isinstance (value , dict ):
158- raise ConfigurationError (
159- f"Provisioning 'copy' step in instance '{ name } ' must have a dictionary value."
160- )
161- self ._validate_copy_step (value , name )
162-
163- if key == "ssh" :
164- self ._validate_ssh_step (value , name )
160+ REGISTERED_PROVISIONERS .get (key )(self .incus , self .reporter ).validate_config (name , value )
165161
166- if key == "llmnr" :
167- self ._validate_llmnr_step (value , name )
168-
169- def _validate_copy_step (self , value , name ):
170- required_fields = ["source" , "target" ]
171- missing = [field for field in required_fields if field not in value ]
172- if missing :
173- raise ConfigurationError (
174- (
175- f"Provisioning 'copy' step in instance '{ name } ' is missing required "
176- f"field(s): { ', ' .join (missing )} ."
177- )
178- )
179- if not isinstance (value ["source" ], str ) or not isinstance (value ["target" ], str ):
180- raise ConfigurationError (
181- (f"Provisioning 'copy' step in instance '{ name } ' must have string " "'source' and 'target'." )
182- )
162+ def _validate_provisioning (self , instance : InstanceConfig , name : str ):
163+ if instance .provision is None :
164+ return
183165
184- if "uid" in value and not isinstance (value ["uid" ], int ):
185- raise ConfigurationError (
186- (f"Provisioning 'copy' step in instance '{ name } ' has invalid 'uid': " "must be an integer." )
187- )
188- if "gid" in value and not isinstance (value ["gid" ], int ):
189- raise ConfigurationError (
190- (f"Provisioning 'copy' step in instance '{ name } ' has invalid 'gid': " "must be an integer." )
191- )
192- if "mode" in value :
193- mode_val = value ["mode" ]
194- if not isinstance (mode_val , str ):
195- raise ConfigurationError (
196- (
197- f"Provisioning 'copy' step in instance '{ name } ' has invalid 'mode': "
198- "must be a string like '0644'."
199- )
200- )
201- if re .fullmatch (r"[0-7]{3,4}" , mode_val ) is None :
202- raise ConfigurationError (
203- (
204- f"Provisioning 'copy' step in instance '{ name } ' has invalid 'mode': "
205- "must be 3-4 octal digits (e.g., '644' or '0644')."
206- )
207- )
208- if "recursive" in value and not isinstance (value ["recursive" ], bool ):
209- raise ConfigurationError (
210- (
211- f"Provisioning 'copy' step in instance '{ name } ' has invalid 'recursive': "
212- "must be a boolean."
213- )
214- )
215- if "create_dirs" in value and not isinstance (value ["create_dirs" ], bool ):
216- raise ConfigurationError (
217- (
218- f"Provisioning 'copy' step in instance '{ name } ' has invalid "
219- "'create_dirs': must be a boolean."
220- )
221- )
166+ provisions = instance .provision
222167
223- def _validate_ssh_step (self , value , name ):
224- if not isinstance (value , (bool , dict )):
225- raise ConfigurationError (
226- f"Provisioning 'ssh' step in instance '{ name } ' must have a boolean " "or dictionary value."
227- )
168+ # Handle special "script" single-step provisioning.
169+ if isinstance (provisions , str ):
170+ provisions = [provisions ]
228171
229- def _validate_llmnr_step (self , value , name ):
230- if not isinstance (value , bool ):
172+ if isinstance (provisions , list ):
173+ for step_idx , step in enumerate (provisions ):
174+ self ._validate_provision_step (step , step_idx , name )
175+ else :
231176 raise ConfigurationError (
232- f"Provisioning 'llmnr' step in instance '{ name } ' must have a boolean value ."
177+ f"Provisioning for instance '{ name } ' must be a string or a list of steps ."
233178 )
234179
235- def _validate_provisioning (self , instance : InstanceConfig , name : str ):
236- if instance .provision is not None :
237- provisions = instance .provision
238- if isinstance (provisions , list ):
239- for step_idx , step in enumerate (provisions ):
240- self ._validate_provision_step (step , step_idx , name )
241- elif not isinstance (provisions , str ):
242- raise ConfigurationError (
243- f"Provisioning for instance '{ name } ' must be a string or a list of steps."
244- )
245-
246180 def _validate_pre_launch (self , instance : InstanceConfig , name : str ):
247181 if instance .pre_launch_cmds is not None :
248182 pre_launch_cmds = instance .pre_launch_cmds
@@ -261,7 +195,6 @@ def validate_config(self):
261195 raise ConfigurationError ("No instances found in config" )
262196
263197 for name , instance_config in self .instance_configs .items ():
264-
265198 # Validate 'provision' field
266199 self ._validate_provisioning (instance_config , name )
267200 self ._validate_pre_launch (instance_config , name )
0 commit comments