Skip to content

Commit d1fee16

Browse files
committed
dhcp with dynamic devfs rules
1 parent 9c18ab7 commit d1fee16

File tree

7 files changed

+487
-3
lines changed

7 files changed

+487
-3
lines changed

libiocage/lib/DevfsRules.py

Lines changed: 382 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,382 @@
1+
import re
2+
import os.path
3+
4+
import libiocage.lib.errors
5+
import libiocage.lib.helpers
6+
7+
8+
class DevfsRulesFilter:
9+
10+
def __init__(self, source, active_filters=[]):
11+
self.source = source
12+
self.active_filters = active_filters
13+
14+
def with_rule(self, rule):
15+
return DevfsRulesFilter(filter(
16+
lambda x: x.has_rule(rule),
17+
self.source
18+
), self.active_filters + [rule])
19+
20+
def with_include(self, rule_name):
21+
rule = f"add include ${rule_name}"
22+
return self.with_rule(rule)
23+
24+
def create_ruleset(self, ruleset_name, ruleset_number):
25+
"""
26+
Create a ruleset from the provided filters
27+
28+
Args:
29+
30+
ruleset_name (string):
31+
Name of the rule that get's created
32+
33+
ruleset_number (int):
34+
Number of the rule that get's created
35+
"""
36+
37+
ruleset = DevfsRuleset(ruleset_name, ruleset_number)
38+
for line in self.active_filters:
39+
ruleset.append(line)
40+
return ruleset
41+
42+
43+
class DevfsRuleset(list):
44+
"""
45+
Representation of a devfs ruleset in the devfs.rules file
46+
47+
DevfsRuleset instances behave like standard lists and can store strings
48+
that multiple lines (string)
49+
"""
50+
51+
PATTERN = re.compile(r"""^\[(?P<name>[a-z](?:[a-z0-9\-_]*[a-z0-9])?)=
52+
(?P<number>[0-9]+)\]\s*(?:\#\s*(?P<comment>.*))?$""", re.X)
53+
54+
def __init__(self, value=None, number=None, comment=None):
55+
"""
56+
Initialize DevfsRuleset
57+
58+
Args:
59+
60+
value (string): (optional)
61+
If specified in combination with a number, this parameter is
62+
interpreted as the ruleset name. Otherwise it is parsed with
63+
the expectation to find a [<name>=<number>] ruleset definition.
64+
65+
When value is not specified or None, DevfsRuleset is assumed
66+
to be new or unspecified (cannot be exported before name and
67+
number were assigned at a later time)
68+
69+
number (int): (optional)
70+
The number of the ruleset. Must be specified to export the
71+
ruleset, but is like the name not required to compare the
72+
ruleset with another
73+
"""
74+
75+
# when only one argument is passed, it's a line that need to be parsed
76+
if value is None and number is None:
77+
# name and number will be assigned later
78+
name = None
79+
elif number is None:
80+
name, number, comment = self._parse_line(value)
81+
else:
82+
name = int(value)
83+
84+
self.name = name
85+
self.number = number
86+
self.comment = comment
87+
list.__init__(self)
88+
89+
def has_rule(self, rule):
90+
"""
91+
Returns true if the rule is part of the current ruleset
92+
93+
Args:
94+
95+
rule (string):
96+
The rule string to be compared with current rules of the
97+
ruleset instance
98+
"""
99+
return rule in self
100+
101+
def append(self, rule):
102+
if rule not in self:
103+
list.append(self, rule)
104+
105+
def clone(self, source_ruleset):
106+
"""
107+
Clones the rules from another ruleset
108+
109+
Args:
110+
111+
source_ruleset (libiocage.lib.DevfsRules.DevfsRuleset):
112+
Ruleset to copy all rules from
113+
"""
114+
for rule in source_ruleset:
115+
self.append(rule)
116+
117+
def _parse_line(self, line):
118+
119+
# marks beginning of a new ruleset
120+
ruleset_match = re.search(DevfsRuleset.PATTERN, line)
121+
if ruleset_match is not None:
122+
name = str(ruleset_match.group("name"))
123+
number = int(ruleset_match.group("number"))
124+
comment = ruleset_match.group("comment")
125+
return name, number, comment
126+
127+
raise SyntaxError("DevfsRuleset line parsing failed")
128+
129+
def __str__(self):
130+
ruleset_line = f"[{self.name}={self.number}]"
131+
if self.comment is not None:
132+
ruleset_line += f" # {self.comment}"
133+
output = [ruleset_line] + [str(x) for x in self]
134+
return "\n".join(output) + "\n"
135+
136+
137+
class DevfsRules(list):
138+
"""
139+
Abstraction for the hosts /etc/devfs.rules
140+
141+
Read and edit devfs rules in a programmatic way.
142+
Restarts devfs service after applying changes.
143+
"""
144+
145+
def __init__(self, rules_file="/etc/devfs.rules", logger=None):
146+
"""
147+
Initializes a DevfsRules manager for devfs.rules files
148+
149+
Args:
150+
151+
rules_file (string): (default=/etc/devfs.rules)
152+
Path of the devfs.rules file
153+
154+
logger (libiocage.Logger): (optional)
155+
Instance of the logger that is passed to occuring errors
156+
"""
157+
158+
self.logger = logger
159+
160+
# index rulesets to find duplicated and provide easy access
161+
self._ruleset_number_index = {}
162+
self._ruleset_name_index = {}
163+
164+
# remember all lines that were loaded from defaults (system)
165+
self._system_rule_lines = []
166+
167+
list.__init__(self)
168+
169+
# will automatically read from file - needs to be the last item
170+
self.rules_file = rules_file
171+
172+
def append(self, ruleset, is_system_rule=False):
173+
"""
174+
Add a DevfsRuleset to the list
175+
176+
The rulesets added become indexed, so that lookups and duplication
177+
checks are easy and fast
178+
179+
Args:
180+
181+
ruleset (libiocage.lib.DevfsRules.DevfsRuleset|string):
182+
The ruleset that gets added if it is not already in the list
183+
"""
184+
185+
next_line_index = len(self)
186+
187+
if ruleset is None or isinstance(ruleset, str):
188+
list.append(self, ruleset)
189+
if is_system_rule is True:
190+
self._system_rule_lines.append(next_line_index)
191+
return ruleset
192+
193+
if ruleset.name in self._ruleset_name_index.keys():
194+
raise libiocage.lib.errors.DuplicateDevfsRuleset(
195+
reason=f"Ruleset named '{ruleset.name}' already present",
196+
devfs_rules_file=self.rules_file,
197+
logger=self.logger
198+
)
199+
200+
if ruleset.number in self._ruleset_number_index.keys():
201+
raise libiocage.lib.errors.DuplicateDevfsRuleset(
202+
reason=f"Ruleset number '{ruleset.number}' already present",
203+
devfs_rules_file=self.rules_file,
204+
logger=self.logger
205+
)
206+
207+
# build indexes
208+
self._ruleset_number_index[ruleset.number] = next_line_index
209+
self._ruleset_name_index[ruleset.name] = next_line_index
210+
if is_system_rule is True:
211+
self._system_rule_lines.append(next_line_index)
212+
213+
list.append(self, ruleset)
214+
return ruleset
215+
216+
def new_ruleset(self, ruleset):
217+
"""
218+
Append a new ruleset
219+
220+
Similar to append(), but automatically assigns a new number
221+
222+
Args:
223+
224+
ruleset (libiocage.lib.DevfsRules.DevfsRuleset):
225+
The new devfs ruleset that is going to be added
226+
227+
Returns:
228+
229+
int: The devfs ruleset number of the created ruleset
230+
"""
231+
232+
ruleset.number = self.next_number
233+
234+
if ruleset.name is None:
235+
ruleset.name = f"iocage_auto_{ruleset.number}"
236+
237+
self.append(ruleset)
238+
return ruleset.number
239+
240+
def find_by_name(self, rule_name):
241+
return self._find_by_index(rule_name, self._ruleset_name_index)
242+
243+
def find_by_number(self, rule_number):
244+
return self._find_by_index(rule_number, self._ruleset_number_index)
245+
246+
def _find_by_index(self, rule_name, index):
247+
return self[index[rule_name]]
248+
249+
@property
250+
def default_rules_file(self):
251+
return "/etc/defaults/devfs.rules"
252+
253+
@property
254+
def rules_file(self):
255+
"""
256+
Path of the devfs.rules file
257+
"""
258+
return self._rules_file
259+
260+
@rules_file.setter
261+
def rules_file(self, devfs_rules_path):
262+
"""
263+
When setting a new devfs.rules source, it is read automatically
264+
"""
265+
self._rules_file = devfs_rules_path
266+
try:
267+
self.read_rules()
268+
except FileNotFoundError:
269+
pass
270+
271+
@property
272+
def next_number(self):
273+
"""
274+
The next highest ruleset number that is available
275+
276+
This counting includes the systems default devfs rulesets
277+
"""
278+
return len(self._ruleset_name_index.keys()) + 1
279+
280+
def read_rules(self):
281+
"""
282+
Read existing devfs.rules file
283+
284+
Existing devfs rules get reset and read from the rules_file
285+
"""
286+
287+
if self.logger:
288+
self.logger.debug(f"Reading devfs.rules from {self.rules_file}")
289+
290+
self.clear()
291+
self._read_rules_file(self.default_rules_file, system=True)
292+
self._read_rules_file(self.rules_file)
293+
294+
def _read_rules_file(self, file, system=False):
295+
296+
f = open(file, "r")
297+
298+
current_ruleset = None
299+
300+
for line in f.readlines():
301+
302+
line = line.strip().rstrip("\n")
303+
304+
# add comments and empty lines as string
305+
if line.startswith("#") or (line == ""):
306+
self.append(line, is_system_rule=system)
307+
continue
308+
309+
try:
310+
current_ruleset = DevfsRuleset(line)
311+
self.append(current_ruleset, is_system_rule=system)
312+
continue
313+
except SyntaxError:
314+
pass
315+
316+
# the first item must be a ruleset
317+
if current_ruleset is None:
318+
raise libiocage.lib.errors.InvalidDevfsRulesSyntax(
319+
devfs_rules_file=self.rules_file,
320+
reason="Rules must follow a ruleset declaration",
321+
logger=self.logger
322+
)
323+
324+
current_ruleset.append(line)
325+
326+
f.close()
327+
328+
def save(self):
329+
"""
330+
Apply changes to the devfs.rules file
331+
332+
Automatically restarts devfs service when the file was changed
333+
"""
334+
335+
content_before = None
336+
337+
if os.path.isfile(self.rules_file):
338+
f = open(self.rules_file, "r+")
339+
content_before = f.read()
340+
f.seek(0)
341+
else:
342+
f = open(self.rules_file, "w")
343+
344+
new_content = self.__str__()
345+
346+
if content_before == new_content:
347+
if self.logger is not None:
348+
self.logger.verbose(
349+
f"devfs.rules file {self.rules_file} unchanged"
350+
)
351+
else:
352+
if self.logger is not None:
353+
self.logger.verbose(
354+
f"Writing devfs.rules to {self.rules_file}"
355+
)
356+
self.logger.spam(new_content, indent=1)
357+
358+
f.write(new_content)
359+
f.truncate()
360+
self._restart_devfs_service()
361+
362+
f.close()
363+
364+
def _restart_devfs_service(self):
365+
"""
366+
Restart devfs service after changing devfs.rules
367+
"""
368+
if self.logger is not None:
369+
self.logger.debug("Restarting devfs service")
370+
libiocage.lib.helpers.exec(["service", "devfs", "restart"])
371+
372+
def __str__(self):
373+
"""
374+
Output the devfs.rules content as string
375+
"""
376+
377+
out_lines = []
378+
for i, line in enumerate(self):
379+
if i not in self._system_rule_lines:
380+
out_lines.append(str(line))
381+
382+
return "\n".join(out_lines)

0 commit comments

Comments
 (0)