Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame^] | 1 | # |
| 2 | # Chris Lumens <clumens@redhat.com> |
| 3 | # |
| 4 | # Copyright 2006, 2007, 2008 Red Hat, Inc. |
| 5 | # |
| 6 | # This copyrighted material is made available to anyone wishing to use, modify, |
| 7 | # copy, or redistribute it subject to the terms and conditions of the GNU |
| 8 | # General Public License v.2. This program is distributed in the hope that it |
| 9 | # will be useful, but WITHOUT ANY WARRANTY expressed or implied, including the |
| 10 | # implied warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
| 11 | # See the GNU General Public License for more details. |
| 12 | # |
| 13 | # You should have received a copy of the GNU General Public License along with |
| 14 | # this program; if not, write to the Free Software Foundation, Inc., 51 |
| 15 | # Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Any Red Hat |
| 16 | # trademarks that are incorporated in the source code or documentation are not |
| 17 | # subject to the GNU General Public License and may only be used or replicated |
| 18 | # with the express permission of Red Hat, Inc. |
| 19 | # |
| 20 | """ |
| 21 | Base classes for creating commands and syntax version object. |
| 22 | |
| 23 | This module exports several important base classes: |
| 24 | |
| 25 | BaseData - The base abstract class for all data objects. Data objects |
| 26 | are contained within a BaseHandler object. |
| 27 | |
| 28 | BaseHandler - The base abstract class from which versioned kickstart |
| 29 | handler are derived. Subclasses of BaseHandler hold |
| 30 | BaseData and KickstartCommand objects. |
| 31 | |
| 32 | DeprecatedCommand - An abstract subclass of KickstartCommand that should |
| 33 | be further subclassed by users of this module. When |
| 34 | a subclass is used, a warning message will be |
| 35 | printed. |
| 36 | |
| 37 | KickstartCommand - The base abstract class for all kickstart commands. |
| 38 | Command objects are contained within a BaseHandler |
| 39 | object. |
| 40 | """ |
| 41 | import gettext |
| 42 | gettext.textdomain("pykickstart") |
| 43 | _ = lambda x: gettext.ldgettext("pykickstart", x) |
| 44 | |
| 45 | import types |
| 46 | import warnings |
| 47 | from pykickstart.errors import * |
| 48 | from pykickstart.ko import * |
| 49 | from pykickstart.parser import Packages |
| 50 | from pykickstart.version import versionToString |
| 51 | |
| 52 | ### |
| 53 | ### COMMANDS |
| 54 | ### |
| 55 | class KickstartCommand(KickstartObject): |
| 56 | """The base class for all kickstart commands. This is an abstract class.""" |
| 57 | removedKeywords = [] |
| 58 | removedAttrs = [] |
| 59 | |
| 60 | def __init__(self, writePriority=0, *args, **kwargs): |
| 61 | """Create a new KickstartCommand instance. This method must be |
| 62 | provided by all subclasses, but subclasses must call |
| 63 | KickstartCommand.__init__ first. Instance attributes: |
| 64 | |
| 65 | currentCmd -- The name of the command in the input file that |
| 66 | caused this handler to be run. |
| 67 | currentLine -- The current unprocessed line from the input file |
| 68 | that caused this handler to be run. |
| 69 | handler -- A reference to the BaseHandler subclass this |
| 70 | command is contained withing. This is needed to |
| 71 | allow referencing of Data objects. |
| 72 | lineno -- The current line number in the input file. |
| 73 | writePriority -- An integer specifying when this command should be |
| 74 | printed when iterating over all commands' __str__ |
| 75 | methods. The higher the number, the later this |
| 76 | command will be written. All commands with the |
| 77 | same priority will be written alphabetically. |
| 78 | """ |
| 79 | |
| 80 | # We don't want people using this class by itself. |
| 81 | if self.__class__ is KickstartCommand: |
| 82 | raise TypeError, "KickstartCommand is an abstract class." |
| 83 | |
| 84 | KickstartObject.__init__(self, *args, **kwargs) |
| 85 | |
| 86 | self.writePriority = writePriority |
| 87 | |
| 88 | # These will be set by the dispatcher. |
| 89 | self.currentCmd = "" |
| 90 | self.currentLine = "" |
| 91 | self.handler = None |
| 92 | self.lineno = 0 |
| 93 | |
| 94 | # If a subclass provides a removedKeywords list, remove all the |
| 95 | # members from the kwargs list before we start processing it. This |
| 96 | # ensures that subclasses don't continue to recognize arguments that |
| 97 | # were removed. |
| 98 | for arg in filter(kwargs.has_key, self.removedKeywords): |
| 99 | kwargs.pop(arg) |
| 100 | |
| 101 | def __call__(self, *args, **kwargs): |
| 102 | """Set multiple attributes on a subclass of KickstartCommand at once |
| 103 | via keyword arguments. Valid attributes are anything specified in |
| 104 | a subclass, but unknown attributes will be ignored. |
| 105 | """ |
| 106 | for (key, val) in kwargs.items(): |
| 107 | # Ignore setting attributes that were removed in a subclass, as |
| 108 | # if they were unknown attributes. |
| 109 | if key in self.removedAttrs: |
| 110 | continue |
| 111 | |
| 112 | if hasattr(self, key): |
| 113 | setattr(self, key, val) |
| 114 | |
| 115 | def __str__(self): |
| 116 | """Return a string formatted for output to a kickstart file. This |
| 117 | method must be provided by all subclasses. |
| 118 | """ |
| 119 | return KickstartObject.__str__(self) |
| 120 | |
| 121 | def parse(self, args): |
| 122 | """Parse the list of args and set data on the KickstartCommand object. |
| 123 | This method must be provided by all subclasses. |
| 124 | """ |
| 125 | raise TypeError, "parse() not implemented for KickstartCommand" |
| 126 | |
| 127 | def apply(self, instroot="/"): |
| 128 | """Write out the configuration related to the KickstartCommand object. |
| 129 | Subclasses which do not provide this method will not have their |
| 130 | configuration written out. |
| 131 | """ |
| 132 | return |
| 133 | |
| 134 | def dataList(self): |
| 135 | """For commands that can occur multiple times in a single kickstart |
| 136 | file (like network, part, etc.), return the list that we should |
| 137 | append more data objects to. |
| 138 | """ |
| 139 | return None |
| 140 | |
| 141 | def deleteRemovedAttrs(self): |
| 142 | """Remove all attributes from self that are given in the removedAttrs |
| 143 | list. This method should be called from __init__ in a subclass, |
| 144 | but only after the superclass's __init__ method has been called. |
| 145 | """ |
| 146 | for attr in filter(lambda k: hasattr(self, k), self.removedAttrs): |
| 147 | delattr(self, attr) |
| 148 | |
| 149 | # Set the contents of the opts object (an instance of optparse.Values |
| 150 | # returned by parse_args) as attributes on the KickstartCommand object. |
| 151 | # It's useful to call this from KickstartCommand subclasses after parsing |
| 152 | # the arguments. |
| 153 | def _setToSelf(self, optParser, opts): |
| 154 | self._setToObj(optParser, opts, self) |
| 155 | |
| 156 | # Sets the contents of the opts object (an instance of optparse.Values |
| 157 | # returned by parse_args) as attributes on the provided object obj. It's |
| 158 | # useful to call this from KickstartCommand subclasses that handle lists |
| 159 | # of objects (like partitions, network devices, etc.) and need to populate |
| 160 | # a Data object. |
| 161 | def _setToObj(self, optParser, opts, obj): |
| 162 | for key in filter (lambda k: getattr(opts, k) != None, optParser.keys()): |
| 163 | setattr(obj, key, getattr(opts, key)) |
| 164 | |
| 165 | class DeprecatedCommand(KickstartCommand): |
| 166 | """Specify that a command is deprecated and no longer has any function. |
| 167 | Any command that is deprecated should be subclassed from this class, |
| 168 | only specifying an __init__ method that calls the superclass's __init__. |
| 169 | This is an abstract class. |
| 170 | """ |
| 171 | def __init__(self, writePriority=None, *args, **kwargs): |
| 172 | # We don't want people using this class by itself. |
| 173 | if self.__class__ is KickstartCommand: |
| 174 | raise TypeError, "DeprecatedCommand is an abstract class." |
| 175 | |
| 176 | # Create a new DeprecatedCommand instance. |
| 177 | KickstartCommand.__init__(self, writePriority, *args, **kwargs) |
| 178 | |
| 179 | def __str__(self): |
| 180 | """Placeholder since DeprecatedCommands don't work anymore.""" |
| 181 | return "" |
| 182 | |
| 183 | def parse(self, args): |
| 184 | """Print a warning message if the command is seen in the input file.""" |
| 185 | mapping = {"lineno": self.lineno, "cmd": self.currentCmd} |
| 186 | warnings.warn(_("Ignoring deprecated command on line %(lineno)s: The %(cmd)s command has been deprecated and no longer has any effect. It may be removed from future releases, which will result in a fatal error from kickstart. Please modify your kickstart file to remove this command.") % mapping, DeprecationWarning) |
| 187 | |
| 188 | |
| 189 | ### |
| 190 | ### HANDLERS |
| 191 | ### |
| 192 | class BaseHandler(KickstartObject): |
| 193 | """Each version of kickstart syntax is provided by a subclass of this |
| 194 | class. These subclasses are what users will interact with for parsing, |
| 195 | extracting data, and writing out kickstart files. This is an abstract |
| 196 | class. |
| 197 | |
| 198 | version -- The version this syntax handler supports. This is set by |
| 199 | a class attribute of a BaseHandler subclass and is used to |
| 200 | set up the command dict. It is for read-only use. |
| 201 | """ |
| 202 | version = None |
| 203 | |
| 204 | def __init__(self, mapping=None, dataMapping=None, commandUpdates=None, |
| 205 | dataUpdates=None, *args, **kwargs): |
| 206 | """Create a new BaseHandler instance. This method must be provided by |
| 207 | all subclasses, but subclasses must call BaseHandler.__init__ first. |
| 208 | |
| 209 | mapping -- A custom map from command strings to classes, |
| 210 | useful when creating your own handler with |
| 211 | special command objects. It is otherwise unused |
| 212 | and rarely needed. If you give this argument, |
| 213 | the mapping takes the place of the default one |
| 214 | and so must include all commands you want |
| 215 | recognized. |
| 216 | dataMapping -- This is the same as mapping, but for data |
| 217 | objects. All the same comments apply. |
| 218 | commandUpdates -- This is similar to mapping, but does not take |
| 219 | the place of the defaults entirely. Instead, |
| 220 | this mapping is applied after the defaults and |
| 221 | updates it with just the commands you want to |
| 222 | modify. |
| 223 | dataUpdates -- This is the same as commandUpdates, but for |
| 224 | data objects. |
| 225 | |
| 226 | |
| 227 | Instance attributes: |
| 228 | |
| 229 | commands -- A mapping from a string command to a KickstartCommand |
| 230 | subclass object that handles it. Multiple strings can |
| 231 | map to the same object, but only one instance of the |
| 232 | command object should ever exist. Most users should |
| 233 | never have to deal with this directly, as it is |
| 234 | manipulated internally and called through dispatcher. |
| 235 | currentLine -- The current unprocessed line from the input file |
| 236 | that caused this handler to be run. |
| 237 | packages -- An instance of pykickstart.parser.Packages which |
| 238 | describes the packages section of the input file. |
| 239 | platform -- A string describing the hardware platform, which is |
| 240 | needed only by system-config-kickstart. |
| 241 | scripts -- A list of pykickstart.parser.Script instances, which is |
| 242 | populated by KickstartParser.addScript and describes the |
| 243 | %pre/%post/%traceback script section of the input file. |
| 244 | """ |
| 245 | |
| 246 | # We don't want people using this class by itself. |
| 247 | if self.__class__ is BaseHandler: |
| 248 | raise TypeError, "BaseHandler is an abstract class." |
| 249 | |
| 250 | KickstartObject.__init__(self, *args, **kwargs) |
| 251 | |
| 252 | # This isn't really a good place for these, but it's better than |
| 253 | # everything else I can think of. |
| 254 | self.scripts = [] |
| 255 | self.packages = Packages() |
| 256 | self.platform = "" |
| 257 | |
| 258 | # These will be set by the dispatcher. |
| 259 | self.commands = {} |
| 260 | self.currentLine = 0 |
| 261 | |
| 262 | # A dict keyed by an integer priority number, with each value being a |
| 263 | # list of KickstartCommand subclasses. This dict is maintained by |
| 264 | # registerCommand and used in __str__. No one else should be touching |
| 265 | # it. |
| 266 | self._writeOrder = {} |
| 267 | |
| 268 | self._registerCommands(mapping, dataMapping, commandUpdates, dataUpdates) |
| 269 | |
| 270 | def __str__(self): |
| 271 | """Return a string formatted for output to a kickstart file.""" |
| 272 | retval = "" |
| 273 | |
| 274 | if self.platform != "": |
| 275 | retval += "#platform=%s\n" % self.platform |
| 276 | |
| 277 | retval += "#version=%s\n" % versionToString(self.version) |
| 278 | |
| 279 | lst = self._writeOrder.keys() |
| 280 | lst.sort() |
| 281 | |
| 282 | for prio in lst: |
| 283 | for obj in self._writeOrder[prio]: |
| 284 | retval += obj.__str__() |
| 285 | |
| 286 | for script in self.scripts: |
| 287 | retval += script.__str__() |
| 288 | |
| 289 | retval += self.packages.__str__() |
| 290 | |
| 291 | return retval |
| 292 | |
| 293 | def _insertSorted(self, lst, obj): |
| 294 | length = len(lst) |
| 295 | i = 0 |
| 296 | |
| 297 | while i < length: |
| 298 | # If the two classes have the same name, it's because we are |
| 299 | # overriding an existing class with one from a later kickstart |
| 300 | # version, so remove the old one in favor of the new one. |
| 301 | if obj.__class__.__name__ > lst[i].__class__.__name__: |
| 302 | i += 1 |
| 303 | elif obj.__class__.__name__ == lst[i].__class__.__name__: |
| 304 | lst[i] = obj |
| 305 | return |
| 306 | elif obj.__class__.__name__ < lst[i].__class__.__name__: |
| 307 | break |
| 308 | |
| 309 | if i >= length: |
| 310 | lst.append(obj) |
| 311 | else: |
| 312 | lst.insert(i, obj) |
| 313 | |
| 314 | def _setCommand(self, cmdObj): |
| 315 | # Add an attribute on this version object. We need this to provide a |
| 316 | # way for clients to access the command objects. We also need to strip |
| 317 | # off the version part from the front of the name. |
| 318 | if cmdObj.__class__.__name__.find("_") != -1: |
| 319 | name = unicode(cmdObj.__class__.__name__.split("_", 1)[1]) |
| 320 | else: |
| 321 | name = unicode(cmdObj.__class__.__name__).lower() |
| 322 | |
| 323 | setattr(self, name.lower(), cmdObj) |
| 324 | |
| 325 | # Also, add the object into the _writeOrder dict in the right place. |
| 326 | if cmdObj.writePriority is not None: |
| 327 | if self._writeOrder.has_key(cmdObj.writePriority): |
| 328 | self._insertSorted(self._writeOrder[cmdObj.writePriority], cmdObj) |
| 329 | else: |
| 330 | self._writeOrder[cmdObj.writePriority] = [cmdObj] |
| 331 | |
| 332 | def _registerCommands(self, mapping=None, dataMapping=None, commandUpdates=None, |
| 333 | dataUpdates=None): |
| 334 | if mapping == {} or mapping == None: |
| 335 | from pykickstart.handlers.control import commandMap |
| 336 | cMap = commandMap[self.version] |
| 337 | else: |
| 338 | cMap = mapping |
| 339 | |
| 340 | if dataMapping == {} or dataMapping == None: |
| 341 | from pykickstart.handlers.control import dataMap |
| 342 | dMap = dataMap[self.version] |
| 343 | else: |
| 344 | dMap = dataMapping |
| 345 | |
| 346 | if type(commandUpdates) == types.DictType: |
| 347 | cMap.update(commandUpdates) |
| 348 | |
| 349 | if type(dataUpdates) == types.DictType: |
| 350 | dMap.update(dataUpdates) |
| 351 | |
| 352 | for (cmdName, cmdClass) in cMap.iteritems(): |
| 353 | # First make sure we haven't instantiated this command handler |
| 354 | # already. If we have, we just need to make another mapping to |
| 355 | # it in self.commands. |
| 356 | cmdObj = None |
| 357 | |
| 358 | for (key, val) in self.commands.iteritems(): |
| 359 | if val.__class__.__name__ == cmdClass.__name__: |
| 360 | cmdObj = val |
| 361 | break |
| 362 | |
| 363 | # If we didn't find an instance in self.commands, create one now. |
| 364 | if cmdObj == None: |
| 365 | cmdObj = cmdClass() |
| 366 | self._setCommand(cmdObj) |
| 367 | |
| 368 | # Finally, add the mapping to the commands dict. |
| 369 | self.commands[cmdName] = cmdObj |
| 370 | self.commands[cmdName].handler = self |
| 371 | |
| 372 | # We also need to create attributes for the various data objects. |
| 373 | # No checks here because dMap is a bijection. At least, that's what |
| 374 | # the comment says. Hope no one screws that up. |
| 375 | for (dataName, dataClass) in dMap.iteritems(): |
| 376 | setattr(self, dataName, dataClass) |
| 377 | |
| 378 | def dispatcher(self, args, lineno): |
| 379 | """Call the appropriate KickstartCommand handler for the current line |
| 380 | in the kickstart file. A handler for the current command should |
| 381 | be registered, though a handler of None is not an error. Returns |
| 382 | the data object returned by KickstartCommand.parse. |
| 383 | |
| 384 | args -- A list of arguments to the current command |
| 385 | lineno -- The line number in the file, for error reporting |
| 386 | """ |
| 387 | cmd = args[0] |
| 388 | |
| 389 | if not self.commands.has_key(cmd): |
| 390 | raise KickstartParseError, formatErrorMsg(lineno, msg=_("Unknown command: %s" % cmd)) |
| 391 | elif self.commands[cmd] != None: |
| 392 | self.commands[cmd].currentCmd = cmd |
| 393 | self.commands[cmd].currentLine = self.currentLine |
| 394 | self.commands[cmd].lineno = lineno |
| 395 | |
| 396 | # The parser returns the data object that was modified. This could |
| 397 | # be a BaseData subclass that should be put into a list, or it |
| 398 | # could be the command handler object itself. |
| 399 | obj = self.commands[cmd].parse(args[1:]) |
| 400 | lst = self.commands[cmd].dataList() |
| 401 | if lst is not None: |
| 402 | lst.append(obj) |
| 403 | |
| 404 | return obj |
| 405 | |
| 406 | def maskAllExcept(self, lst): |
| 407 | """Set all entries in the commands dict to None, except the ones in |
| 408 | the lst. All other commands will not be processed. |
| 409 | """ |
| 410 | self._writeOrder = {} |
| 411 | |
| 412 | for (key, val) in self.commands.iteritems(): |
| 413 | if not key in lst: |
| 414 | self.commands[key] = None |
| 415 | |
| 416 | def hasCommand(self, cmd): |
| 417 | """Return true if there is a handler for the string cmd.""" |
| 418 | return hasattr(self, cmd) |
| 419 | |
| 420 | |
| 421 | ### |
| 422 | ### DATA |
| 423 | ### |
| 424 | class BaseData(KickstartObject): |
| 425 | """The base class for all data objects. This is an abstract class.""" |
| 426 | removedKeywords = [] |
| 427 | removedAttrs = [] |
| 428 | |
| 429 | def __init__(self, *args, **kwargs): |
| 430 | """Create a new BaseData instance. |
| 431 | |
| 432 | lineno -- Line number in the ks-file where this object was defined |
| 433 | """ |
| 434 | |
| 435 | # We don't want people using this class by itself. |
| 436 | if self.__class__ is BaseData: |
| 437 | raise TypeError, "BaseData is an abstract class." |
| 438 | |
| 439 | KickstartObject.__init__(self, *args, **kwargs) |
| 440 | self.lineno = 0 |
| 441 | |
| 442 | def __str__(self): |
| 443 | """Return a string formatted for output to a kickstart file.""" |
| 444 | return "" |
| 445 | |
| 446 | def __call__(self, *args, **kwargs): |
| 447 | """Set multiple attributes on a subclass of BaseData at once via |
| 448 | keyword arguments. Valid attributes are anything specified in a |
| 449 | subclass, but unknown attributes will be ignored. |
| 450 | """ |
| 451 | for (key, val) in kwargs.items(): |
| 452 | # Ignore setting attributes that were removed in a subclass, as |
| 453 | # if they were unknown attributes. |
| 454 | if key in self.removedAttrs: |
| 455 | continue |
| 456 | |
| 457 | if hasattr(self, key): |
| 458 | setattr(self, key, val) |
| 459 | |
| 460 | def deleteRemovedAttrs(self): |
| 461 | """Remove all attributes from self that are given in the removedAttrs |
| 462 | list. This method should be called from __init__ in a subclass, |
| 463 | but only after the superclass's __init__ method has been called. |
| 464 | """ |
| 465 | for attr in filter(lambda k: hasattr(self, k), self.removedAttrs): |
| 466 | delattr(self, attr) |