Skip to content

Commit af7b8f4

Browse files
committed
Prototyped dnf_appstream custom promise type
I wasn't looking at the ticket prior to implementation, likely missing things. Ticket: CFE-3653
1 parent 23b4a0d commit af7b8f4

1 file changed

Lines changed: 223 additions & 0 deletions

File tree

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
#!/usr/bin/python3
2+
#
3+
# Custom promise type to manage DNF AppStream modules
4+
# Uses cfengine_module_library.py library.
5+
#
6+
# Use it in the policy like this:
7+
# promise agent dnf_appstream
8+
# {
9+
# interpreter => "/usr/bin/python3";
10+
# path => "$(sys.inputdir)/dnf_appstream.py";
11+
# }
12+
# bundle agent main
13+
# {
14+
# dnf_appstream:
15+
# "nodejs"
16+
# state => "enabled",
17+
# stream => "12";
18+
# }
19+
20+
import dnf
21+
import re
22+
from cfengine_module_library import PromiseModule, ValidationError, Result
23+
24+
25+
class DnfAppStreamPromiseTypeModule(PromiseModule):
26+
def __init__(self, **kwargs):
27+
super(DnfAppStreamPromiseTypeModule, self).__init__(
28+
name="dnf_appstream_promise_module", version="0.0.1", **kwargs
29+
)
30+
31+
# Define all expected attributes with their types and validation
32+
self.add_attribute("state", str, required=True, default="enabled",
33+
validator=lambda x: self._validate_state(x))
34+
self.add_attribute("stream", str, required=False)
35+
self.add_attribute("profile", str, required=False)
36+
37+
def _validate_state(self, value):
38+
if value not in ("enabled", "disabled", "installed", "removed"):
39+
raise ValidationError("state attribute must be 'enabled', 'disabled', 'installed', or 'removed'")
40+
41+
def _validate_module_name(self, name):
42+
# Validate module name to prevent injection
43+
if not re.match(r'^[a-zA-Z0-9_.-]+$', name):
44+
raise ValidationError(f"Invalid module name: {name}. Only alphanumeric, underscore, dot, and dash characters are allowed.")
45+
46+
def _validate_stream_name(self, stream):
47+
# Validate stream name to prevent injection
48+
if stream and not re.match(r'^[a-zA-Z0-9_.-]+$', stream):
49+
raise ValidationError(f"Invalid stream name: {stream}. Only alphanumeric, underscore, dot, and dash characters are allowed.")
50+
51+
def validate_promise(self, promiser, attributes, meta):
52+
# Validate promiser (module name)
53+
if not isinstance(promiser, str):
54+
raise ValidationError("Promiser must be of type string")
55+
56+
self._validate_module_name(promiser)
57+
58+
# Validate stream if provided
59+
if "stream" in attributes:
60+
self._validate_stream_name(attributes["stream"])
61+
62+
def evaluate_promise(self, promiser, attributes, meta):
63+
module_name = promiser
64+
state = attributes.get("state", "enabled")
65+
stream = attributes.get("stream", None)
66+
profile = attributes.get("profile", None)
67+
68+
# Construct the module specification
69+
module_spec = module_name
70+
if stream:
71+
module_spec += ":" + stream
72+
if profile:
73+
module_spec += "/" + profile
74+
75+
try:
76+
# Create a DNF base object
77+
base = dnf.Base()
78+
79+
# Read configuration
80+
base.conf.assumeyes = True
81+
82+
# Read repository information
83+
base.read_all_repos()
84+
85+
# Fill the sack (package database)
86+
base.fill_sack(load_system_repo='auto')
87+
88+
# Access the module base
89+
module_base = base.module_base
90+
if module_base is None:
91+
self.log_error("DNF modules are not available")
92+
return Result.NOT_KEPT
93+
94+
# Check current state of the module
95+
current_state = self._get_module_state(module_base, module_name, stream)
96+
97+
# Determine what action to take based on desired state
98+
if state == "enabled":
99+
if current_state == "enabled":
100+
self.log_verbose(f"Module {module_name} is already enabled")
101+
return Result.KEPT
102+
else:
103+
return self._enable_module(module_base, module_spec)
104+
elif state == "disabled":
105+
if current_state == "disabled":
106+
self.log_verbose(f"Module {module_name} is already disabled")
107+
return Result.KEPT
108+
else:
109+
return self._disable_module(module_base, module_spec)
110+
elif state == "installed":
111+
if current_state in ["installed", "enabled"]:
112+
# For "installed" state, if it's already installed or enabled,
113+
# we need to install packages from it
114+
# But if it's already installed with packages, we're done
115+
if self._is_module_installed_with_packages(base, module_name, stream):
116+
self.log_verbose(f"Module {module_name} is already installed with packages")
117+
return Result.KEPT
118+
else:
119+
# Module is enabled but packages are not installed
120+
return self._install_module(module_base, module_spec)
121+
else:
122+
# Module is not enabled, need to install (which will enable and install packages)
123+
return self._install_module(module_base, module_spec)
124+
elif state == "removed":
125+
if current_state == "removed" or current_state == "disabled":
126+
self.log_verbose(f"Module {module_name} is already removed or disabled")
127+
return Result.KEPT
128+
else:
129+
return self._remove_module(module_base, module_spec)
130+
131+
except Exception as e:
132+
self.log_error(f"Error managing module {module_name}: {str(e)}")
133+
return Result.NOT_KEPT
134+
135+
def _get_module_state(self, module_base, module_name, stream):
136+
"""Get the current state of a module using DNF Python API"""
137+
try:
138+
# List all modules to check the current state
139+
module_list, _ = module_base._get_modules(module_name)
140+
141+
for module in module_list:
142+
# Check if this is the stream we're looking for (if specified)
143+
if stream and module.stream != stream:
144+
continue
145+
146+
# Check the module state
147+
if module.status == "enabled":
148+
return "enabled"
149+
elif module.status == "disabled":
150+
return "disabled"
151+
elif module.status == "installed":
152+
return "installed"
153+
154+
# If we get here, module is not found or not in the specified stream
155+
return "removed"
156+
157+
except Exception as e:
158+
self.log_error(f"Error getting module state for {module_name}: {str(e)}")
159+
return "unknown"
160+
161+
def _is_module_installed_with_packages(self, base, module_name, stream):
162+
"""Check if the module packages are actually installed on the system"""
163+
try:
164+
# Check if packages from the module are installed
165+
# This is a more complex check that requires examining installed packages
166+
# to see if they are from the specified module
167+
return False # Simplified for now - would need more complex logic
168+
except Exception:
169+
return False
170+
171+
def _enable_module(self, module_base, module_spec):
172+
"""Enable a module using DNF Python API"""
173+
try:
174+
module_base.enable([module_spec])
175+
module_base.base.resolve()
176+
module_base.base.do_transaction()
177+
self.log_verbose(f"Module {module_spec} enabled successfully")
178+
return Result.REPAIRED
179+
except Exception as e:
180+
self.log_error(f"Failed to enable module {module_spec}: {str(e)}")
181+
return Result.NOT_KEPT
182+
183+
def _disable_module(self, module_base, module_spec):
184+
"""Disable a module using DNF Python API"""
185+
try:
186+
module_base.disable([module_spec])
187+
module_base.base.resolve()
188+
module_base.base.do_transaction()
189+
self.log_verbose(f"Module {module_spec} disabled successfully")
190+
return Result.REPAIRED
191+
except Exception as e:
192+
self.log_error(f"Failed to disable module {module_spec}: {str(e)}")
193+
return Result.NOT_KEPT
194+
195+
def _install_module(self, module_base, module_spec):
196+
"""Install a module (enable + install default packages) using DNF Python API"""
197+
try:
198+
# Enable and install the module
199+
module_base.install([module_spec])
200+
module_base.base.resolve()
201+
module_base.base.do_transaction()
202+
self.log_verbose(f"Module {module_spec} installed successfully")
203+
return Result.REPAIRED
204+
except Exception as e:
205+
self.log_error(f"Failed to install module {module_spec}: {str(e)}")
206+
return Result.NOT_KEPT
207+
208+
def _remove_module(self, module_base, module_spec):
209+
"""Remove a module using DNF Python API"""
210+
try:
211+
# Get list of packages from the module to remove
212+
module_base.remove([module_spec])
213+
module_base.base.resolve()
214+
module_base.base.do_transaction()
215+
self.log_verbose(f"Module {module_spec} removed successfully")
216+
return Result.REPAIRED
217+
except Exception as e:
218+
self.log_error(f"Failed to remove module {module_spec}: {str(e)}")
219+
return Result.NOT_KEPT
220+
221+
222+
if __name__ == "__main__":
223+
DnfAppStreamPromiseTypeModule().start()

0 commit comments

Comments
 (0)