Source code for utah.preseed

# Ubuntu Testing Automation Harness
# Copyright 2012 Canonical Ltd.

# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 3, as published
# by the Free Software Foundation.

# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranties of
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
# PURPOSE.  See the GNU General Public License for more details.

# You should have received a copy of the GNU General Public License along
# with this program.  If not, see <http://www.gnu.org/licenses/>.

r"""This module provides all the classes needed to:
 - Parse a preseed file
 - Update some values
 - Add new sections
 - Write the changes back to a file

The expected way to use it is by passing a file-like object or an iterable that
yields one line of the preseed at a time:

>>> from utah.preseed import Preseed
>>> from StringIO import StringIO
>>> preseed_text = StringIO(
...    '# Comment\n'
...    '\n'
...    'd-i passwd/username string utah\n')
>>> preseed = Preseed(preseed_text)

After that, any of the configuration sections can be accessed by the question
name:

>>> section = preseed['passwd/username']
>>> section
<ConfigurationSection: 'd-i passwd/username string utah\n'>

and values can be updated by setting them directly in the section objects:

>>> section.value = 'ubuntu'
>>> section
<ConfigurationSection: 'd-i passwd/username string ubuntu\n'>

In addition to this, if a new section is needed, it can be appended/prepended
to the preseed by calling directly the :class:`Preseed` methods or the
:class:`ConfigurationSection` methods to use the section as a reference, that
is, append/prepend after/before the given section.

>>> section.append('d-i passwd/user-password password\n')
>>> section.append('d-i passwd/user-password-again password\n')

Once the desired changes have been applied, the :meth:`Preseed.dump` method can
be used to write the output to a new file:

>>> print preseed.dump()
# Comment
<BLANKLINE>
d-i passwd/username string ubuntu
d-i passwd/user-password-again password
d-i passwd/user-password password
<BLANKLINE>

"""


import string


[docs]class Preseed(object): """Read/Write preseed files easily. :param lines: File-like object or iterable that yields one line from the preseed at a time. :type lines: iterable """ def __init__(self, lines=None): # Used to access quickly to configuration sections by question name self._qnames = {} if lines is not None: self.load(lines) else: self.sections = [] def __getitem__(self, key): """Access lines directly by their question name. :param key: Question name :type: `basestring` | :class:`TextPropertyValue` :returns: Section in the preseed that matches the passed question name :rtype: :class:`Section` """ if isinstance(key, TextPropertyValue): key = key.text if not isinstance(key, basestring): raise TypeError return self._qnames[key] def __contains__(self, key): """Use in operator with question names. :param key: Question name :type: `basestring` | `TextPropertyValue` :returns: Whether a section that matches the question name is in the preseed or not :rtype: `bool` """ if isinstance(key, TextPropertyValue): key = key.text if not isinstance(key, basestring): raise TypeError return key in self._qnames
[docs] def load(self, lines): r"""Parse preseed configuration lines. :param lines: Any iterable that yields preseed file configuration lines :type lines: iterable :returns: Preseed file object with information parsed :rtype: :class:`Preseed` :raises ParsingError: If there's a problem parsing some configuration lines, this exception will be raised with the line number where the problem was detected in the message. :Example: >>> from StringIO import StringIO >>> preseed_text = StringIO( ... '# Comment\n' ... '\n' ... 'd-i passwd/username string utah\n') >>> preseed = Preseed() >>> preseed.load(preseed_text) >>> print preseed.dump() # Comment <BLANKLINE> d-i passwd/username string utah <BLANKLINE> .. note:: This method is automatically called at initialization time if the `lines` parameter is passed to the constructor, so it's not really expected to be used directly. .. seealso:: :meth:`dump` """ self.sections = [] # One line might be made of multiple lines # that are continued with '\\' output_lines = [] for line_number, input_line in enumerate(lines): input_line = input_line.rstrip('\r\n') output_lines.append(input_line) # Line is finished when it's a comment # or when no continuation character is found if (input_line.startswith('#') or not input_line.endswith('\\')): try: new_section = Section.new(output_lines) except ParsingError as exception: # Add line number information to the exception raise ParsingError("Line {}: {}" .format(line_number + 1, exception)) self.append(new_section) output_lines = []
[docs] def dump(self): r"""Dump preseed configuration statements. This method returns the contents of the preseed after the changes applied. The string returned is normally used to write the changes back to a file that can be used as the new preseed to provision a system. :returns: Formatted preseed configuration lines :rtype: `string` :Example: >>> preseed = Preseed('# Comment\n'.splitlines()) >>> preseed.dump() '# Comment\n' .. seealso:: :meth:`load` """ return ''.join(str(section) for section in self.sections)
[docs] def prepend(self, new_section, ref_section=None): r"""Prepend a new section to the preseed. :param new_section: The new section to be prepended. .. note:: If a string is passed instead, a new section will be created from the string with :meth:`Section.new`. :type new_section: :class:`Section` | `basestring` :param ref_section: A section to be used as a reference, meaning that the new section will be prepended after the reference section. .. note:: If no reference section is passed, then the new section will be prepended just to the beginning of the preseed. :type ref_section: :class:`Section` :Example: >>> preseed = Preseed('d-i passwd/username string utah\n'.splitlines()) >>> preseed.prepend('# Comment') >>> print preseed.dump() # Comment d-i passwd/username string utah <BLANKLINE> .. seealso:: :meth:`append` """ if isinstance(new_section, basestring): new_section = Section.new(new_section.splitlines()) assert isinstance(new_section, Section) assert new_section.parent is None if ref_section is None: index = 0 else: for index, section in enumerate(self.sections): if section is ref_section: break else: raise ValueError('Reference section not found: {}' .format(ref_section)) if (self.sections and isinstance(new_section, (BlankSection, CommentSection)) and type(new_section) == type(self.sections[index])): # Old section to be replaced, won't have a parent anymore self.sections[index].parent = None grouped_section = new_section + self.sections[index] self.sections[index] = grouped_section # New section is now included in the preseed grouped_section.parent = self else: self._insert(index, new_section)
[docs] def append(self, new_section, ref_section=None): r"""Append a new section to the preseed. :param new_section: The new section to be appended. .. note:: If a string is passed instead, a new section will be created from the string with :meth:`Section.new`. :type new_section: :class:`Section` | `basestring` :param ref_section: A section to be used as a reference, meaning that the new section will be appended after the reference section. .. note:: If no reference section is passed, then the new section will be appended just to the end of the preseed. :type ref_section: :class:`Section` :Example: >>> preseed = Preseed('# Comment\n'.splitlines()) >>> preseed.append('d-i passwd/username string utah\n') >>> print preseed.dump() # Comment d-i passwd/username string utah <BLANKLINE> .. seealso:: :meth:`prepend` """ if isinstance(new_section, basestring): new_section = Section.new(new_section.splitlines()) assert isinstance(new_section, Section) assert new_section.parent is None if ref_section is None: index = len(self.sections) - 1 else: for index, section in enumerate(self.sections): if section is ref_section: break else: raise ValueError('Reference section not found: {}' .format(ref_section)) if (self.sections and isinstance(new_section, (BlankSection, CommentSection)) and type(new_section) == type(self.sections[index])): self.sections[index] += new_section else: index += 1 self._insert(index, new_section)
def _insert(self, index, new_section): """Insert section or join it with another one of the same type.""" assert new_section.parent is None # Take ownership of the section new_section.parent = self self.sections.insert(index, new_section) # Update question name index if isinstance(new_section, ConfigurationSection): self.section_updated(new_section, 'qname', None, new_section.qname.text)
[docs] def section_updated(self, section, property_name, old_text, new_text): """Update question names index. This is a callback called every time a section property is updated and used to maintain question names index integrity :param section: Section object calling the callback :type section: :class:`Section` :param property_name: Name of the updated property :type property_name: `string` :param old_text: Old property text :type old_text: `string` | `None` :param new_text: New property text :type new_text: `string` :raises DuplicatedQuestionName: If the updated property is `qname` and the new text is already taken by other section which would break the access to a section by the question name. """ assert isinstance(new_text, basestring) if property_name == 'qname': if new_text in self._qnames: raise DuplicatedQuestionName(new_text) if old_text is not None: assert isinstance(old_text, basestring) assert old_text in self._qnames assert self._qnames[old_text] == section del self._qnames[old_text] self._qnames[new_text] = section
[docs]class Section(object): """Any kind of preseed section (blank, comment or configuration).""" def __init__(self): self.parent = None def __repr__(self): return '<{}: {!r}>'.format(self.__class__.__name__, str(self)) def __str__(self): return '<Section>' @classmethod
[docs] def new(cls, lines): r"""Create new section subclass based on the lines in the preseed. This method is used by the :meth:`Preseed.load` method to create new sections while parsing a preseed file. :param lines: Lines to be parsed for this particular section :type lines: `list` :returns: Section object the properly represents the lines passed :rtype: subclass of :class:`Section` :Example: >>> from utah.preseed import Section >>> Section.new('\n'.splitlines()) <BlankSection: '\n'> >>> Section.new('# Comment\n'.splitlines()) <CommentSection: '# Comment\n'> >>> Section.new('d-i passwd/username string utah\n'.splitlines()) <ConfigurationSection: 'd-i passwd/username string utah\n'> .. seealso:: :class:`BlankSection`, :class:`CommentSection`, :class:`ConfigurationSection` """ assert isinstance(lines, list) if all(not line for line in lines): return BlankSection(len(lines)) if all(line.startswith('#') for line in lines): return CommentSection(lines) assert all(line and not line.startswith('#') for line in lines) return ConfigurationSection(lines)
[docs]class BlankSection(Section): """A pressed section that represents a group of consecutive blank lines. :param lines_count: Number of blank lines represented by this section :type lines_count: `int` :var lines_count: The number of lines as passed to the constructor :Example: >>> from utah.preseed import BlankSection >>> section = BlankSection(3) >>> section.lines_count 3 .. automethod:: __add__ .. automethod:: __iadd__ """ def __init__(self, lines_count): super(BlankSection, self).__init__() assert isinstance(lines_count, int) self.lines_count = lines_count def __str__(self): return '\n' * self.lines_count
[docs] def __add__(self, other): r"""Add two blank sections. :param other: The section to add :type other: :class:`BlankSection` :returns: A new blank section that contains the amount of lines lines from the two sections being added. :rtype: :class:`BlankSection` :Example: >>> blank1 = BlankSection(1) >>> blank2 = BlankSection(2) >>> blank3 = blank1 + blank2 >>> blank3.lines_count 3 .. seealso:: :meth:`__iadd__` """ assert isinstance(other, BlankSection) assert self.parent == other.parent return BlankSection(self.lines_count + other.lines_count)
[docs] def __iadd__(self, other): r"""Add two blank sections in place. :param other: The section to add :type other: :class:`BlankSection` :returns: The section on the left updated to contain the amount of lines lines from the two sections being added. :rtype: :class:`BlankSection` :Example: >>> blank1 = BlankSection(1) >>> blank2 = BlankSection(2) >>> blank3 = blank1 + blank2 >>> blank3.lines_count 3 .. seealso:: :meth:`__iadd__` """ assert isinstance(other, BlankSection) self.lines_count += other.lines_count return self
[docs]class CommentSection(Section): r"""A preseed section that represents a group consecutive comment lines. :param lines: An iterable that yields one line at a time :type lines: `iterable` :var lines: The comment lines that were passed to the constructor :Example: >>> from utah.preseed import CommentSection >>> comment_str = '# Comment\n' >>> section = CommentSection(comment_str.splitlines()) >>> section.lines ['# Comment'] .. automethod:: __add__ .. automethod:: __iadd__ """ def __init__(self, lines): super(CommentSection, self).__init__() assert isinstance(lines, list) assert all(line.startswith('#') for line in lines) self.lines = lines def __str__(self): return '{}\n'.format('\n'.join(self.lines))
[docs] def __add__(self, other): r"""Add two comment sections. :param other: The section to add :type other: :class:`CommentSection` :returns: A new comment section that contains the comment lines from the two sections being added. :rtype: :class:`CommentSection` :Example: >>> comment1 = CommentSection('# Comment 1\n'.splitlines()) >>> comment2 = CommentSection('# Comment 2\n'.splitlines()) >>> comment3 = comment1 + comment2 >>> comment3.lines ['# Comment 1', '# Comment 2'] .. seealso:: :meth:`__iadd__` """ assert isinstance(other, CommentSection) assert self.parent == other.parent return CommentSection(self.lines + other.lines)
[docs] def __iadd__(self, other): r"""Add two comment sections in place. :param other: The section to add :type other: :class:`CommentSection` :returns: The section on the left updated to contain the comment lines from the two sections being added. :rtype: :class:`CommentSection` :Example: >>> comment1 = CommentSection('# Comment 1\n'.splitlines()) >>> comment2 = CommentSection('# Comment 2\n'.splitlines()) >>> comment1 += comment2 >>> comment1.lines ['# Comment 1', '# Comment 2'] .. seealso:: :meth:`__add__` """ assert isinstance(other, CommentSection) self.lines.extend(other.lines) return self
[docs]class TextProperty(object): """A text property used in :class:`ConfigurationSection` objects. .. seealso:: :class:`TextPropertyValue` """ def __init__(self, name): self.name = name self.obj_name = '_{}'.format(name) def __get__(self, obj, owner=None): if not hasattr(obj, self.obj_name): self.__set__(obj, '') value = getattr(obj, self.obj_name) assert isinstance(value, TextPropertyValue) return value def __set__(self, obj, new_text): assert isinstance(new_text, basestring) if hasattr(obj, self.obj_name): old_value = self.__get__(obj) old_text = old_value.text else: old_text = None new_value = TextPropertyValue(self, obj, new_text) setattr(obj, self.obj_name, new_value) obj.property_updated(self.name, old_text, new_text)
[docs]class TextPropertyValue(object): """A text value used in :class:`TextProperty` objects. The value being stored is just a text string, so there's currently no type even if the configuration sections in a preseed use types for values. """ def __init__(self, parent, obj, text=''): self.parent = parent self.obj = obj self.text = text def __str__(self): return self.text def __repr__(self): return '<TextPropertyValue: {!r}>'.format(self.text) def __nonzero__(self): return bool(self.text) def __eq__(self, other): if isinstance(other, TextPropertyValue): return self.text == other.text if isinstance(other, basestring): return self.text == other raise TypeError
[docs] def prepend(self, other_text): r"""Prepend a string to the stored value. :param other_text: The text to be prepended :type other_text: `basestring` :returns: The updated value :rtype: :class:`TextPropertyValue` :Example: >>> late_command_str = 'd-i preseed/late_command string some_command\n' >>> section = Section.new(late_command_str.splitlines()) >>> section.value <TextPropertyValue: 'some_command'> >>> section.value.prepend('another_command; ') <TextPropertyValue: 'another_command; some_command'> .. note:: The change happens in place, so there's no need to assign any result back to the :class:`TextProperty` object: """ assert isinstance(other_text, basestring) old_text = self.text new_text = other_text + self.text self.text = new_text self.obj.property_updated(self.parent.name, old_text, new_text) return self
[docs] def append(self, other_text): r"""Append a string to the stored value. :param other_text: The text to be appended :type other_text: `basestring` :returns: The updated value :rtype: :class:`TextPropertyValue` :Example: >>> late_command_str = 'd-i preseed/late_command string some_command\n' >>> section = Section.new(late_command_str.splitlines()) >>> section.value <TextPropertyValue: 'some_command'> >>> section.value.append('; another_command') <TextPropertyValue: 'some_command; another_command'> .. note:: The change happens in place, so there's no need to assign any result back to the :class:`TextProperty` object: """ assert isinstance(other_text, basestring) old_text = self.text new_text = self.text + other_text self.text = new_text self.obj.property_updated(self.parent.name, old_text, new_text) return self
[docs]class ConfigurationSection(Section): r"""A preseed configuration statement made of one or multiple lines. The expected format of a configuration section is as follows:: <owner> <qname> <qtype> <value> where the whole section might be made of multiple lines. A line is considered not to finish the statement if there's a backslash character just before the newline character. If the parsing succeeds, every field is accessible using the same name as above. :parameter raw_lines: An iterable that yields one line at a time :type raw_lines: `iterable` :raises ParsingError: If the configuration lines don't follow the expected format above, this exception will be raised. :Example: >>> from utah.preseed import ConfigurationSection >>> configuration_str = 'd-i passwd/username string utah\n' >>> section = ConfigurationSection(configuration_str.splitlines()) >>> section.owner <TextPropertyValue: 'd-i'> >>> section.qname <TextPropertyValue: 'passwd/username'> >>> section.qtype <TextPropertyValue: 'string'> >>> section.value <TextPropertyValue: 'utah'> """ TRAILING_CHARS = string.whitespace + '\\' owner = TextProperty('owner') qname = TextProperty('qname') qtype = TextProperty('qtype') value = TextProperty('value') def __init__(self, raw_lines): super(ConfigurationSection, self).__init__() lines = [raw_lines[0].rstrip(self.TRAILING_CHARS)] for raw_line in raw_lines[1:]: lines.append(raw_line .lstrip() .rstrip(self.TRAILING_CHARS)) text = ' '.join(lines) splitted_text = text.split(None, 3) try: self.owner = splitted_text[0] self.qname = splitted_text[1] self.qtype = splitted_text[2] except IndexError: raise ParsingError('Unable to parse configuration lines: {}' .format(text)) if len(splitted_text) == 4: self.value = splitted_text[3] else: self.value = '' def __str__(self): """Return text representation in a single line.""" if self.value: line = ('{} {} {} {}\n' .format(self.owner, self.qname, self.qtype, self.value)) else: line = ('{} {} {}\n' .format(self.owner, self.qname, self.qtype)) return line
[docs] def prepend(self, new_section): r"""Prepend a new section to this one. This is a wrapper method that actually calls the :meth:`Preseed.prepend` method in the preseed using this section as a reference section to set the insertion position. :param new_section: The new section to be prepended. .. note:: If a string is passed instead, a new section will be created from the string with :meth:`Section.new`. :type new_section: :class:`Section` | `basestring` :returns: None :rtype: None :Example: >>> preseed = Preseed() >>> section = Section.new('d-i passwd/username string utah\n' ... .splitlines()) >>> preseed.append(section) >>> section.prepend('d-i passwd/user-password password\n') >>> print preseed.dump() d-i passwd/user-password password d-i passwd/username string utah <BLANKLINE> .. seealso:: :meth:`append` """ assert self.parent is not None if isinstance(new_section, basestring): new_section = Section.new(new_section.splitlines()) assert isinstance(new_section, Section) self.parent.prepend(new_section, self)
[docs] def append(self, new_section): r"""Append a new section to this one. This is a wrapper method that actually calls the :meth:`Preseed.append` method in the preseed using this section as a reference section to set the insertion position. :param new_section: The new section to be appended. .. note:: If a string is passed instead, a new section will be created from the string with :meth:`Section.new`. :type new_section: :class:`Section` | `basestring` :returns: None :rtype: None :Example: >>> preseed = Preseed() >>> section = Section.new('d-i passwd/username string utah\n' ... .splitlines()) >>> preseed.append(section) >>> section.append('d-i passwd/user-password password\n') >>> print preseed.dump() d-i passwd/username string utah d-i passwd/user-password password <BLANKLINE> .. seealso:: :meth:`prepend` """ assert self.parent is not None if isinstance(new_section, basestring): new_section = Section.new(new_section.splitlines()) assert isinstance(new_section, Section) self.parent.append(new_section, self)
[docs] def property_updated(self, property_name, old_value, new_value): """Propagate property updates to preseed parent. If a parent preseed is set, for every updated received from a property value, the same update is propagated to the parent preseed object. :param property_name: Name of the updated property :type property_name: string :param old_value: Old property value :type old_value: `string` | `None` :param new_value: New property value :type new_value: `string` """ if self.parent: self.parent.section_updated(self, property_name, old_value, new_value)
[docs]class ParsingError(Exception): r"""An error happened when parsing a preseed section. This exception is raised when a preseed is being parsed and a new section object is being created to store some contents. :Example: >>> preseed_str = ('not valid\n') >>> preseed = Preseed(preseed_str.splitlines()) Traceback (most recent call last): ... ParsingError: Line 1: Unable to parse configuration lines: not valid .. seealso:: :meth:`Preseed.load`, :class:`ConfigurationSection` """ pass
[docs]class DuplicatedQuestionName(Exception): r"""Duplicated question name found in preseed. This exception is raised when a question name is found more than once in a preseed. This is part of the process used in the `Preseed` class to guarantee that questions can be accessed based on their name. :Example: >>> preseed_str = ('d-i passwd/username string utah\n' ... 'd-i passwd/username string ubuntu\n') >>> preseed = Preseed(preseed_str.splitlines()) Traceback (most recent call last): ... DuplicatedQuestionName: passwd/username """ pass
Read the Docs v: latest
Versions
latest
Downloads
PDF
HTML
Epub
On Read the Docs
Project Home
Builds

Free document hosting provided by Read the Docs.