1111import curses
1212import datetime
1313import glob
14+ import importlib
1415import json
1516import os
1617import platform
@@ -108,6 +109,7 @@ class Installer(object):
108109 'prepkgsinstallscripts' ,
109110 'public_key' ,
110111 'photon_docker_image' ,
112+ 'plugins' ,
111113 'repos' ,
112114 'search_path' ,
113115 'setup_grub_script' ,
@@ -132,6 +134,7 @@ def __init__(self, working_directory=Defaults.WORKING_DIRECTORY, rpm_path=None,
132134 self .rpm_path = rpm_path
133135 self .log_path = log_path
134136 self .logger = None
137+ self .loaded_plugins = []
135138 self .cmd = None
136139 self .working_directory = working_directory
137140 self .photon_release_version = photon_release_version
@@ -142,6 +145,7 @@ def __init__(self, working_directory=Defaults.WORKING_DIRECTORY, rpm_path=None,
142145 self .window = None # Initialize to prevent AttributeError
143146
144147 # some keys can have arch specific variations
148+ self .known_keys = set (Installer .known_keys )
145149 for key in ['packages' , 'linux_flavor' ]:
146150 for arch in ['x86_64' , 'aarch64' ]:
147151 self .known_keys .add (f'{ key } _{ arch } ' )
@@ -201,10 +205,15 @@ def configure(self, install_config, ui_config=None):
201205 config = IsoConfig ()
202206 install_config = curses .wrapper (config .configure , ui_config )
203207
208+ self .install_config = install_config
209+ self ._load_plugins (install_config )
210+
204211 # _check_install_config will raise InstallerConfigError if there's an issue
205212 self ._check_install_config (install_config )
213+ self ._execute_external_plugins (modules .commons .CHECK_CONFIG )
206214
207215 self ._add_defaults (install_config )
216+ self ._execute_external_plugins (modules .commons .ADD_DEFAULTS )
208217
209218 self .tdnf = tdnf .Tdnf (logger = self .logger ,
210219 config_file = self .tdnf_conf_path ,
@@ -213,8 +222,6 @@ def configure(self, install_config, ui_config=None):
213222 releasever = self .photon_release_version ,
214223 installroot = self .photon_root )
215224
216- self .install_config = install_config
217-
218225 self .ab_present = self ._is_ab_present ()
219226 self ._prepare_devices ()
220227 self ._get_disk_sizes ()
@@ -557,7 +564,7 @@ def _check_install_config(self, install_config):
557564 Raises InstallerConfigError if the configuration is invalid.
558565 """
559566
560- unknown_keys = install_config .keys () - Installer .known_keys
567+ unknown_keys = install_config .keys () - self .known_keys
561568 if len (unknown_keys ) > 0 :
562569 raise InstallerConfigError ("Unknown install_config keys: " + ", " .join (unknown_keys ))
563570
@@ -1642,6 +1649,54 @@ def _setup_grub(self):
16421649
16431650 self ._setup_grub_password ()
16441651
1652+ def _load_plugins (self , install_config ):
1653+ """
1654+ Load external plugins and add their known keys to the installer.
1655+ """
1656+
1657+ plugins_to_load = ['photon_installer.plugins' ]
1658+ plugins_to_load .extend (install_config .get ('plugins' , []))
1659+
1660+ for plugin_name in plugins_to_load :
1661+ try :
1662+ plugin_mod = importlib .import_module (plugin_name )
1663+ except ImportError as e :
1664+ if plugin_name == 'photon_installer.plugins' :
1665+ continue
1666+ self .logger .error (f"Error importing plugin { plugin_name } : { e } " )
1667+ raise InstallerError (f"Failed to load plugin { plugin_name } : { e } " )
1668+
1669+ self .loaded_plugins .append (plugin_mod )
1670+
1671+ # Let plugins register their own known keys
1672+ if hasattr (plugin_mod , 'known_keys' ):
1673+ self .known_keys .update (plugin_mod .known_keys )
1674+
1675+ # If the plugin has a get_known_keys function, call it
1676+ if hasattr (plugin_mod , 'get_known_keys' ):
1677+ try :
1678+ self .known_keys .update (plugin_mod .get_known_keys ())
1679+ except Exception as e :
1680+ self .logger .warning (f"Failed to get known keys from plugin { plugin_name } : { e } " )
1681+
1682+ def _execute_external_plugins (self , phase ):
1683+ """
1684+ Execute phase-specific functions from external plugins.
1685+ """
1686+ # Convert phase string (e.g. 'pre-install') to function name (e.g. 'pre_install')
1687+ func_name = phase .replace ('-' , '_' )
1688+
1689+ for plugin_mod in self .loaded_plugins :
1690+ if hasattr (plugin_mod , func_name ):
1691+ plugin_name = plugin_mod .__name__
1692+ self .logger .info (f"Executing { func_name } from plugin { plugin_name } " )
1693+ try :
1694+ func = getattr (plugin_mod , func_name )
1695+ func (self )
1696+ except Exception as e :
1697+ self .logger .error (f"Error executing { func_name } in plugin { plugin_name } : { e } " )
1698+ raise InstallerError (f"Plugin { plugin_name } failed during { func_name } : { e } " )
1699+
16451700 def _execute_modules (self , phase ):
16461701 """
16471702 Execute the scripts in the modules folder
@@ -1675,6 +1730,8 @@ def _execute_modules(self, phase):
16751730
16761731 mod .execute (self )
16771732
1733+ self ._execute_external_plugins (phase )
1734+
16781735 def _adjust_packages_based_on_selected_flavor (self ):
16791736 """
16801737 Install slected linux flavor only
@@ -2053,7 +2110,6 @@ def _wait_for_device(device_path, timeout=30, check_interval=0.1):
20532110 Wait for a device node to appear and be accessible.
20542111 Returns True if device appears, False if timeout.
20552112 """
2056- import time
20572113 elapsed = 0
20582114 while elapsed < timeout :
20592115 if os .path .exists (device_path ):
@@ -2442,6 +2498,8 @@ def _final_check(self):
24422498 content = f .read ().strip ()
24432499 assert content == "uninitialized" or content == "" , f"file { machine_id_file } content is { content } , but should be 'uninitialized' or empty"
24442500
2501+ self ._execute_external_plugins (modules .commons .FINAL_CHECK )
2502+
24452503 def getfile (self , filename ):
24462504 """
24472505 Returns absolute filepath by filename.
0 commit comments