Merge lp:~javier.collado/utah/preseed-parser into lp:utah

Proposed by Javier Collado
Status: Merged
Approved by: Javier Collado
Approved revision: 673
Merged at revision: 671
Proposed branch: lp:~javier.collado/utah/preseed-parser
Merge into: lp:utah
Diff against target: 473 lines (+400/-51)
2 files modified
utah/preseed.py (+352/-0)
utah/provisioning/provisioning.py (+48/-51)
To merge this branch: bzr merge lp:~javier.collado/utah/preseed-parser
Reviewer Review Type Date Requested Status
Max Brustkern (community) Approve
Review via email: mp+123068@code.launchpad.net

Description of the change

This branch adds a new module to parse the preseed file, modify it and write it easily (the original idea was to get something similar to what the ConfigParser module from the standard library provides).

The features are:
- Quick access to preseeded questions using the question name
- Ability to prepend/append new sections (not only questions, but also comments or blank lines)
- Ability to prepend/append strings to a value in a section (useful to prepend to the late command)

The provisioning module has been updated and I tested the code with the default preseed and I got the same customized preseed as in the development branch.

Please let me know what do you think about these changes.

To post a comment you must log in.
Revision history for this message
Max Brustkern (nuclearbob) wrote :

I like this a lot. Having this would have made a few things easier in cobbler desktop support, so I know it'll be helpful for future integrations. I'd also like UTAH to be able to run an automated install from a broken or empty preseed, and this will really help with that. Good stuff.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'utah/preseed.py'
--- utah/preseed.py 1970-01-01 00:00:00 +0000
+++ utah/preseed.py 2012-09-06 12:53:21 +0000
@@ -0,0 +1,352 @@
1"""
2Pressed files handling
3"""
4import string
5
6
7class Preseed(object):
8 """
9 Read/Write preseed files easily
10 """
11 def __init__(self, filename):
12 # Used to access quickly to configuration sections by question name
13 self._qnames = {}
14
15 self.filename = filename
16 self.read(filename)
17
18 def __getitem__(self, key):
19 """
20 Access lines directly by their question name
21 """
22 if isinstance(key, TextPropertyValue):
23 key = key.text
24 if not isinstance(key, basestring):
25 raise TypeError
26
27 return self._qnames[key]
28
29 def __contains__(self, key):
30 """
31 Use in operator with question names
32 """
33 if isinstance(key, TextPropertyValue):
34 key = key.text
35 if not isinstance(key, basestring):
36 raise TypeError
37
38 return key in self._qnames
39
40 def read(self, filename=None):
41 """
42 Read a whole preseed file and parse its configuration lines
43 """
44 if filename is not None:
45 self.filename = filename
46 else:
47 filename = self.filename
48
49 self.sections = []
50 with open(filename) as f:
51 # One line might be made of multiple lines
52 # that are continued with '\\'
53 output_lines = []
54 for input_line in f:
55 input_line = input_line.rstrip('\n')
56 output_lines.append(input_line)
57
58 # Line is finished only when no continuation character is found
59 if not input_line.endswith('\\'):
60 new_section = Section.new(self, output_lines)
61 self.append(new_section)
62 output_lines = []
63
64 def write(self, filename=None):
65 """
66 Write the modified file to the same or a new location
67 """
68 if filename is not None:
69 self.filename = filename
70 else:
71 filename = self.filename
72
73 with open(filename, 'w') as f:
74 for section in self.sections:
75 f.write('{}'.format(section))
76
77 def append(self, new_section, ref_section=None):
78 """
79 Append a new section
80 """
81 assert isinstance(new_section, Section)
82 if ref_section is None:
83 index = len(self.sections) - 1
84 else:
85 for index, section in enumerate(self.sections):
86 if section is ref_section:
87 break
88 else:
89 raise ValueError('Reference section not found: {}'
90 .format(ref_section))
91
92 if (self.sections and
93 isinstance(new_section, (BlankSection, CommentSection)) and
94 type(new_section) == type(self.sections[index])):
95 self.sections[index] += new_section
96 else:
97 index += 1
98 self._insert(index, new_section)
99
100 def prepend(self, new_section, ref_section=None):
101 """
102 Prepend a new section
103 """
104 assert isinstance(new_section, Section)
105 if ref_section is None:
106 index = 0
107 else:
108 for index, section in enumerate(self.sections):
109 if section is ref_section:
110 break
111 else:
112 raise ValueError('Reference section not found: {}'
113 .format(ref_section))
114
115 if (self.sections and
116 isinstance(new_section, (BlankSection, CommentSection)) and
117 type(new_section) == type(self.sections[index])):
118 self.sections[index] = new_section + self.sections[index]
119 else:
120 self._insert(index, new_section)
121
122 def _insert(self, index, new_section):
123 """
124 Insert section or join it with another one of the same type
125 """
126 self.sections.insert(index, new_section)
127
128 def section_updated(self, section, property_name, old_value, new_value):
129 """
130 Callback called any time a section property is updated
131
132 Used to maintain question names index integrity
133 """
134 if property_name == 'qname':
135 new_text = new_value.text
136 assert new_text not in self._qnames
137
138 if old_value is not None:
139 old_text = old_value.text
140 assert old_text in self._qnames
141 assert self._qnames[old_text] == section
142 del self._qnames[old_text]
143
144 self._qnames[new_text] = section
145
146
147class Section(object):
148 """
149 Any kind of line (blank, comment or configuration)
150 """
151 def __init__(self, parent):
152 self.parent = parent
153
154 def __repr__(self):
155 return '<{}: {!r}>'.format(self.__class__.__name__, str(self))
156
157 @classmethod
158 def new(self, parent, lines):
159 """
160 Create new section subclass based on the lines in the preseed
161 """
162 if isinstance(lines, basestring):
163 lines = [lines]
164 assert isinstance(lines, list)
165
166 if all(not line for line in lines):
167 return BlankSection(parent, len(lines))
168
169 if all(line.startswith('#') for line in lines):
170 return CommentSection(parent, lines)
171
172 assert all(line and not line.startswith('#')
173 for line in lines)
174 return ConfigurationSection(parent, lines)
175
176
177class BlankSection(Section):
178 """
179 Any number of consecutive blank lines
180 """
181 def __init__(self, parent, lines_count):
182 super(BlankSection, self).__init__(parent)
183 assert isinstance(lines_count, int)
184 self.lines_count = lines_count
185
186 def __str__(self):
187 return '\n' * self.lines_count
188
189 def __add__(self, other):
190 assert isinstance(other, BlankSection)
191 return BlankSection(self.lines_count + other.lines_count)
192
193 def __iadd__(self, other):
194 assert isinstance(other, BlankSection)
195 self.lines_count += other.lines_count
196 return self
197
198
199class CommentSection(Section):
200 """
201 Any number of consecutive comment lines
202 """
203 def __init__(self, parent, lines):
204 super(CommentSection, self).__init__(parent)
205 assert isinstance(lines, list)
206 assert all(line.startswith('#') for line in lines)
207 self.lines = lines
208
209 def __str__(self):
210 return '{}\n'.format('\n'.join(self.lines))
211
212 def __add__(self, other):
213 assert isinstance(other, CommentSection)
214 return CommentSection(self.lines + other.lines)
215
216 def __iadd__(self, other):
217 assert isinstance(other, CommentSection)
218 self.lines.extend(other.lines)
219 return self
220
221
222class TextProperty(object):
223 def __init__(self, name):
224 self.name = name
225 self.obj_name = '_{}'.format(name)
226
227 def __get__(self, obj, type=None):
228 if not hasattr(obj, self.obj_name):
229 self.__set__(obj, '')
230 value = getattr(obj, self.obj_name)
231 assert isinstance(value, TextPropertyValue)
232 return value
233
234 def __set__(self, obj, new_text):
235 assert isinstance(new_text, basestring)
236
237 if hasattr(obj, self.obj_name):
238 old_value = self.__get__(obj)
239 else:
240 old_value = None
241 new_value = TextPropertyValue(self, obj, new_text)
242 setattr(obj, self.obj_name, new_value)
243 obj.property_updated(self.name, old_value, new_value)
244
245
246class TextPropertyValue(object):
247 def __init__(self, parent, obj, text=''):
248 self.parent = parent
249 self.obj = obj
250 self.text = text
251
252 def __str__(self):
253 return self.text
254
255 def __repr__(self):
256 return '<TextPropertyValue: {!r}>'.format(self.text)
257
258 def __nonzero__(self):
259 return bool(self.text)
260
261 def __eq__(self, other):
262 if isinstance(other, TextPropertyValue):
263 return self.text == other.text
264
265 if isinstance(other, basestring):
266 return self.text == other
267
268 raise ValueError
269
270 def prepend(self, other_text):
271 """
272 Prepend a string to the stored value
273 """
274 assert isinstance(other_text, basestring)
275 self.text = other_text + self.text
276 return self
277
278 def append(self, other_text):
279 """
280 Append a string to the stored value
281 """
282 assert isinstance(other_text, basestring)
283 self.text = self.text + other_text
284 return self
285
286
287class ConfigurationSection(Section):
288 """
289 A configuration statement made of one or multiple lines
290 """
291 TRAILING_CHARS = string.whitespace + '\\'
292
293 owner = TextProperty('owner')
294 qname = TextProperty('qname')
295 qtype = TextProperty('qtype')
296 value = TextProperty('value')
297
298 def __init__(self, parent, raw_lines):
299 """
300 Parse each element, so that it can be modified if needed
301 """
302 super(ConfigurationSection, self).__init__(parent)
303 lines = [raw_lines[0].rstrip(self.TRAILING_CHARS)]
304 for raw_line in raw_lines[1:]:
305 lines.append(raw_line
306 .lstrip()
307 .rstrip(self.TRAILING_CHARS))
308 text = ' '.join(lines)
309 splitted_text = text.split(None, 3)
310 self.owner = splitted_text[0]
311 self.qname = splitted_text[1]
312 self.qtype = splitted_text[2]
313 if len(splitted_text) == 4:
314 self.value = splitted_text[3]
315 else:
316 self.value = ''
317
318 def __str__(self):
319 """
320 Return text representation in a single line
321 """
322 if self.value:
323 line = ('{} {} {} {}\n'
324 .format(self.owner, self.qname, self.qtype,
325 self.value))
326 else:
327 line = ('{} {} {}\n'
328 .format(self.owner, self.qname, self.qtype))
329 return line
330
331 def prepend(self, new_section):
332 """
333 Prepend a new section to this one
334 """
335 if isinstance(new_section, basestring):
336 new_section = Section.new(self.parent, new_section)
337
338 assert isinstance(new_section, Section)
339 self.parent.prepend(new_section, self)
340
341 def append(self, new_section):
342 """
343 Append a new section to this one
344 """
345 if isinstance(new_section, basestring):
346 new_section = Section.new(self.parent, new_section)
347
348 assert isinstance(new_section, Section)
349 self.parent.append(new_section, self)
350
351 def property_updated(self, property_name, old_value, new_value):
352 self.parent.section_updated(self, property_name, old_value, new_value)
0353
=== modified file 'utah/provisioning/provisioning.py'
--- utah/provisioning/provisioning.py 2012-08-29 18:46:24 +0000
+++ utah/provisioning/provisioning.py 2012-09-06 12:53:21 +0000
@@ -19,6 +19,7 @@
19import apt.cache19import apt.cache
2020
21from utah.iso import ISO21from utah.iso import ISO
22from utah.preseed import Preseed
22from utah.provisioning.exceptions import UTAHProvisioningException23from utah.provisioning.exceptions import UTAHProvisioningException
23from utah.retry import retry24from utah.retry import retry
24import utah.timeout25import utah.timeout
@@ -772,57 +773,53 @@
772 self.logger.info('Setting up preseed')773 self.logger.info('Setting up preseed')
773 if tmpdir is None:774 if tmpdir is None:
774 tmpdir = self.tmpdir775 tmpdir = self.tmpdir
775 preseed = open(os.path.join(tmpdir, 'initrd.d', 'preseed.cfg'), 'w')776 preseed = Preseed(self.preseed)
776 for line in open(self.preseed, 'r'):777 if 'preseed/late_command' in preseed:
777 if 'preseed/late_command string' in line:778 if self.installtype == 'desktop':
778 if self.installtype == 'desktop':779 self.logger.info('Changing d-i latecommand '
779 self.logger.info('Changing d-i latecommand '780 + 'to ubiquity success_command '
780 + 'to ubiquity success_command '781 + 'and prepending ubiquity lines')
781 + 'and prepending ubiquity lines')782 question = preseed['preseed/late_command']
782 line = line.replace('d-i', 'ubiquity')783 question.owner = 'ubiquity'
783 line = line.replace('preseed/late_command string ',784 question.qname = 'ubiquity/success_command'
784 ('ubiquity/success_command '785 question.value.prepend(
785 'string chroot /target sh -c \'' # chroot start786 'chroot /target sh -c \'' # chroot start
786 'export LOG_FILE=/var/log/utah-install; '787 'export LOG_FILE=/var/log/utah-install; '
787 'apt-get install -y --force-yes openssh-server '788 'apt-get install -y --force-yes openssh-server '
788 '>>$LOG_FILE 2>&1 ; '789 '>>$LOG_FILE 2>&1 ; '
789 'apt-get install -y --force-yes gdebi-core '790 'apt-get install -y --force-yes gdebi-core '
790 '>>$LOG_FILE 2>&1 ; '791 '>>$LOG_FILE 2>&1 ; '
791 '\'; '), # chroot end792 '\'; ' # chroot end
792 1)793 )
793 line = ("ubiquity ubiquity/summary note\n"794 question.prepend("ubiquity ubiquity/summary note")
794 "ubiquity ubiquity/reboot boolean true\n"795 question.prepend("ubiquity ubiquity/reboot boolean true")
795 + line)796 else:
796 else:797 self.logger.info('Prepending latecommand')
797 self.logger.info('Prepending latecommand')798 question.value.prepend('sh utah-latecommand ; ')
798 line = line.replace('preseed/late_command string ',799 if 'ubiquity/success_command' in preseed:
799 ('preseed/late_command string '800 self.logger.info('Prepending success_command')
800 'sh utah-latecommand ; '), 1)801 question = preseed['ubiquity/success_command']
801 line = line.rstrip(' ;')802 question.value.prepend('sh utah-latecommand ; ')
802 if 'ubiquity/success_command string' in line:803 if 'pkgsel/include' in preseed:
803 self.logger.info('Prepending success_command')804 question = preseed['pkgsel/include']
804 line = line.replace('ubiquity/success_command string',805 packages = question.value.text.split()
805 ('ubiquity/success_command string '806 for pkgname in ('openssh-server', 'gdebi-core'):
806 'sh utah-latecommand ; '), 1)807 if pkgname not in packages:
807 line = line.rstrip(' ;')808 self.logger.info('Adding {} to preseeded packages'
808 if 'pkgsel/include' in line:809 .format(pkgname))
809 for pkgname in ('openssh-server', 'gdebi-core'):810 packages.append(pkgname)
810 if pkgname not in line:811 question.value = ' '.join(packages)
811 self.logger.info('Adding {} to preseeded packages'812
812 .format(pkgname))813 if 'netcfg/get_hostname' in preseed:
813 line = line.replace('string ',814 self.logger.info('Rewriting hostname to ' + self.name)
814 'string {} '.format(pkgname))815 question = preseed['netcfg/get_hostname']
815 line = line.rstrip() + "\n"816 question.value = self.name
816 if 'netcfg/get_hostname' in line:817 if 'passwd/username' in preseed:
817 self.logger.info('Rewriting hostname to ' + self.name)818 self.logger.info('Rewriting username to ' + config.user)
818 line = 'd-i netcfg/get_hostname string ' + self.name + "\n"819 question = preseed['passwd/username']
819 if 'passwd/username' in line:820 question.value = config.user
820 self.logger.info('Rewriting username to ' + config.user)821
821 line = 'd-i passwd/username string ' + config.user + "\n"822 preseed.write(os.path.join(tmpdir, 'initrd.d', 'preseed.cfg'))
822 #Now that debug keeps the directory, this may not be needed
823 #self.logger.debug('Preseed line: ' + line)
824 preseed.write(line)
825 preseed.flush()
826823
827 if self.installtype == 'desktop':824 if self.installtype == 'desktop':
828 self.logger.info('Inserting preseed into casper')825 self.logger.info('Inserting preseed into casper')

Subscribers

People subscribed via source and target branches