Skip to content

Commit 34c4014

Browse files
oliverkurthGitHub Enterprise
authored andcommitted
Merge pull request #59 from vcf/topic/okurth/custom-checks
add plugin support
2 parents 47dfd8d + 6061ec6 commit 34c4014

6 files changed

Lines changed: 223 additions & 5 deletions

File tree

examples/plugins/example_checks.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import os
2+
3+
4+
def pre_install(installer):
5+
installer.logger.info("Executing pre_install hook from poi_plugins...")
6+
7+
8+
def pre_pkgs_install(installer):
9+
installer.logger.info("Executing pre_pkgs_install hook from poi_plugins...")
10+
11+
12+
def post_install(installer):
13+
installer.logger.info("Executing post_install hook from poi_plugins...")
14+
15+
16+
def final_check(installer):
17+
"""
18+
Run custom validation checks on the installed system.
19+
20+
:param installer: The Installer instance, providing access to:
21+
- installer.photon_root (path to the chroot)
22+
- installer.cmd (CommandUtils for running commands)
23+
- installer.logger (for logging output)
24+
- installer.install_config (the parsed ks config)
25+
"""
26+
installer.logger.info("Starting custom validation checks...")
27+
28+
# ---------------------------------------------------------
29+
# Example 1: Run a command INSIDE the chroot
30+
# ---------------------------------------------------------
31+
installer.logger.info("Checking if SSH is enabled...")
32+
33+
# run_in_chroot executes the command inside the new system and returns the exit code
34+
retval = installer.cmd.run_in_chroot(
35+
installer.photon_root,
36+
"systemctl is-enabled sshd"
37+
)
38+
39+
if retval == 0:
40+
installer.logger.info("Check passed: SSH is enabled.")
41+
else:
42+
# You can log a warning, or raise an exception to fail the installation
43+
installer.logger.warn("Check failed: SSH is NOT enabled.")
44+
# raise Exception("SSH must be enabled!")
45+
46+
# ---------------------------------------------------------
47+
# Example 2: Run a command to check a package
48+
# ---------------------------------------------------------
49+
installer.logger.info("Checking if 'vim' is installed...")
50+
retval = installer.cmd.run_in_chroot(
51+
installer.photon_root,
52+
"rpm -q vim"
53+
)
54+
if retval == 0:
55+
installer.logger.info("Check passed: vim is installed.")
56+
else:
57+
installer.logger.warn("Check failed: vim is not installed.")
58+
59+
# ---------------------------------------------------------
60+
# Example 3: Check a file directly from the host side
61+
# ---------------------------------------------------------
62+
installer.logger.info("Checking GRUB configuration...")
63+
grub_cfg_path = os.path.join(installer.photon_root, "boot/grub2/grub.cfg")
64+
65+
if os.path.exists(grub_cfg_path):
66+
with open(grub_cfg_path, 'r') as f:
67+
content = f.read()
68+
if 'password' in content:
69+
installer.logger.info("Check passed: GRUB password is set.")
70+
else:
71+
installer.logger.warn("Check failed: GRUB password is NOT set.")
72+
else:
73+
installer.logger.warn(f"GRUB config not found at {grub_cfg_path}")
74+
75+
installer.logger.info("Custom validation checks completed.")

photon_installer/installer.py

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import curses
1212
import datetime
1313
import glob
14+
import importlib
1415
import json
1516
import os
1617
import 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.

photon_installer/modules/commons.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
PRE_INSTALL = "pre-install"
1010
PRE_PKGS_INSTALL = "pre-pkgs-install"
1111
POST_INSTALL = "post-install"
12+
FINAL_CHECK = "final-check"
13+
CHECK_CONFIG = "check-config"
14+
ADD_DEFAULTS = "add-defaults"
1215

1316

1417
def replace_string_in_file(filename, search_string, replace_string):
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import importlib
2+
import pkgutil
3+
4+
5+
def _execute_phase(phase_name, installer):
6+
"""
7+
Iterate through all modules in the plugins package
8+
and execute the phase function if it exists.
9+
"""
10+
# __path__ is a special variable available in __init__.py representing the package directory
11+
for _, module_name, ispkg in pkgutil.iter_modules(__path__):
12+
if ispkg or module_name.startswith('_'):
13+
continue # Skip directories and private files like _my_utils.py
14+
15+
full_module_name = f"{__name__}.{module_name}"
16+
try:
17+
# Import the dropped-in module (e.g., plugins.example_checks)
18+
mod = importlib.import_module(full_module_name)
19+
20+
# Check if this specific module implements the current phase
21+
if hasattr(mod, phase_name):
22+
func = getattr(mod, phase_name)
23+
installer.logger.info(f"Executing {phase_name} from {full_module_name}")
24+
func(installer)
25+
26+
except Exception as e:
27+
installer.logger.error(f"Error executing {phase_name} in {full_module_name}: {e}")
28+
raise
29+
30+
# Explicitly define the phase functions so the installer's
31+
# hasattr(plugin_mod, 'pre_install') check succeeds on the plugins package.
32+
33+
34+
def check_config(installer):
35+
_execute_phase('check_config', installer)
36+
37+
38+
def add_defaults(installer):
39+
_execute_phase('add_defaults', installer)
40+
41+
42+
def pre_install(installer):
43+
_execute_phase('pre_install', installer)
44+
45+
46+
def pre_pkgs_install(installer):
47+
_execute_phase('pre_pkgs_install', installer)
48+
49+
50+
def post_install(installer):
51+
_execute_phase('post_install', installer)
52+
53+
54+
def final_check(installer):
55+
_execute_phase('final_check', installer)
56+
57+
58+
def get_known_keys():
59+
"""
60+
Iterate through all modules in the plugins package
61+
and collect their known_keys if they exist.
62+
"""
63+
keys = set()
64+
for _, module_name, ispkg in pkgutil.iter_modules(__path__):
65+
if ispkg or module_name.startswith('_'):
66+
continue
67+
68+
full_module_name = f"{__name__}.{module_name}"
69+
try:
70+
mod = importlib.import_module(full_module_name)
71+
if hasattr(mod, 'known_keys'):
72+
keys.update(mod.known_keys)
73+
if hasattr(mod, 'get_known_keys'):
74+
try:
75+
keys.update(mod.get_known_keys())
76+
except Exception:
77+
pass
78+
except Exception:
79+
# Ignore errors here, they will be caught during execution
80+
pass
81+
return keys

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
[flake8]
22
ignore = E501, W503, W504
3+
exclude = venv

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
setup(
1818
name='photon-installer',
1919
description='Installer code for photon',
20-
packages=find_packages(include=['photon_installer', 'photon_installer.modules']),
20+
packages=find_packages(include=['photon_installer', 'photon_installer.modules', 'photon_installer.plugins']),
2121
install_requires=REQUIRES,
2222
include_package_data=True,
2323
zip_safe=False,

0 commit comments

Comments
 (0)