|
| 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