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
1=== added file 'utah/preseed.py'
2--- utah/preseed.py 1970-01-01 00:00:00 +0000
3+++ utah/preseed.py 2012-09-06 12:53:21 +0000
4@@ -0,0 +1,352 @@
5+"""
6+Pressed files handling
7+"""
8+import string
9+
10+
11+class Preseed(object):
12+ """
13+ Read/Write preseed files easily
14+ """
15+ def __init__(self, filename):
16+ # Used to access quickly to configuration sections by question name
17+ self._qnames = {}
18+
19+ self.filename = filename
20+ self.read(filename)
21+
22+ def __getitem__(self, key):
23+ """
24+ Access lines directly by their question name
25+ """
26+ if isinstance(key, TextPropertyValue):
27+ key = key.text
28+ if not isinstance(key, basestring):
29+ raise TypeError
30+
31+ return self._qnames[key]
32+
33+ def __contains__(self, key):
34+ """
35+ Use in operator with question names
36+ """
37+ if isinstance(key, TextPropertyValue):
38+ key = key.text
39+ if not isinstance(key, basestring):
40+ raise TypeError
41+
42+ return key in self._qnames
43+
44+ def read(self, filename=None):
45+ """
46+ Read a whole preseed file and parse its configuration lines
47+ """
48+ if filename is not None:
49+ self.filename = filename
50+ else:
51+ filename = self.filename
52+
53+ self.sections = []
54+ with open(filename) as f:
55+ # One line might be made of multiple lines
56+ # that are continued with '\\'
57+ output_lines = []
58+ for input_line in f:
59+ input_line = input_line.rstrip('\n')
60+ output_lines.append(input_line)
61+
62+ # Line is finished only when no continuation character is found
63+ if not input_line.endswith('\\'):
64+ new_section = Section.new(self, output_lines)
65+ self.append(new_section)
66+ output_lines = []
67+
68+ def write(self, filename=None):
69+ """
70+ Write the modified file to the same or a new location
71+ """
72+ if filename is not None:
73+ self.filename = filename
74+ else:
75+ filename = self.filename
76+
77+ with open(filename, 'w') as f:
78+ for section in self.sections:
79+ f.write('{}'.format(section))
80+
81+ def append(self, new_section, ref_section=None):
82+ """
83+ Append a new section
84+ """
85+ assert isinstance(new_section, Section)
86+ if ref_section is None:
87+ index = len(self.sections) - 1
88+ else:
89+ for index, section in enumerate(self.sections):
90+ if section is ref_section:
91+ break
92+ else:
93+ raise ValueError('Reference section not found: {}'
94+ .format(ref_section))
95+
96+ if (self.sections and
97+ isinstance(new_section, (BlankSection, CommentSection)) and
98+ type(new_section) == type(self.sections[index])):
99+ self.sections[index] += new_section
100+ else:
101+ index += 1
102+ self._insert(index, new_section)
103+
104+ def prepend(self, new_section, ref_section=None):
105+ """
106+ Prepend a new section
107+ """
108+ assert isinstance(new_section, Section)
109+ if ref_section is None:
110+ index = 0
111+ else:
112+ for index, section in enumerate(self.sections):
113+ if section is ref_section:
114+ break
115+ else:
116+ raise ValueError('Reference section not found: {}'
117+ .format(ref_section))
118+
119+ if (self.sections and
120+ isinstance(new_section, (BlankSection, CommentSection)) and
121+ type(new_section) == type(self.sections[index])):
122+ self.sections[index] = new_section + self.sections[index]
123+ else:
124+ self._insert(index, new_section)
125+
126+ def _insert(self, index, new_section):
127+ """
128+ Insert section or join it with another one of the same type
129+ """
130+ self.sections.insert(index, new_section)
131+
132+ def section_updated(self, section, property_name, old_value, new_value):
133+ """
134+ Callback called any time a section property is updated
135+
136+ Used to maintain question names index integrity
137+ """
138+ if property_name == 'qname':
139+ new_text = new_value.text
140+ assert new_text not in self._qnames
141+
142+ if old_value is not None:
143+ old_text = old_value.text
144+ assert old_text in self._qnames
145+ assert self._qnames[old_text] == section
146+ del self._qnames[old_text]
147+
148+ self._qnames[new_text] = section
149+
150+
151+class Section(object):
152+ """
153+ Any kind of line (blank, comment or configuration)
154+ """
155+ def __init__(self, parent):
156+ self.parent = parent
157+
158+ def __repr__(self):
159+ return '<{}: {!r}>'.format(self.__class__.__name__, str(self))
160+
161+ @classmethod
162+ def new(self, parent, lines):
163+ """
164+ Create new section subclass based on the lines in the preseed
165+ """
166+ if isinstance(lines, basestring):
167+ lines = [lines]
168+ assert isinstance(lines, list)
169+
170+ if all(not line for line in lines):
171+ return BlankSection(parent, len(lines))
172+
173+ if all(line.startswith('#') for line in lines):
174+ return CommentSection(parent, lines)
175+
176+ assert all(line and not line.startswith('#')
177+ for line in lines)
178+ return ConfigurationSection(parent, lines)
179+
180+
181+class BlankSection(Section):
182+ """
183+ Any number of consecutive blank lines
184+ """
185+ def __init__(self, parent, lines_count):
186+ super(BlankSection, self).__init__(parent)
187+ assert isinstance(lines_count, int)
188+ self.lines_count = lines_count
189+
190+ def __str__(self):
191+ return '\n' * self.lines_count
192+
193+ def __add__(self, other):
194+ assert isinstance(other, BlankSection)
195+ return BlankSection(self.lines_count + other.lines_count)
196+
197+ def __iadd__(self, other):
198+ assert isinstance(other, BlankSection)
199+ self.lines_count += other.lines_count
200+ return self
201+
202+
203+class CommentSection(Section):
204+ """
205+ Any number of consecutive comment lines
206+ """
207+ def __init__(self, parent, lines):
208+ super(CommentSection, self).__init__(parent)
209+ assert isinstance(lines, list)
210+ assert all(line.startswith('#') for line in lines)
211+ self.lines = lines
212+
213+ def __str__(self):
214+ return '{}\n'.format('\n'.join(self.lines))
215+
216+ def __add__(self, other):
217+ assert isinstance(other, CommentSection)
218+ return CommentSection(self.lines + other.lines)
219+
220+ def __iadd__(self, other):
221+ assert isinstance(other, CommentSection)
222+ self.lines.extend(other.lines)
223+ return self
224+
225+
226+class TextProperty(object):
227+ def __init__(self, name):
228+ self.name = name
229+ self.obj_name = '_{}'.format(name)
230+
231+ def __get__(self, obj, type=None):
232+ if not hasattr(obj, self.obj_name):
233+ self.__set__(obj, '')
234+ value = getattr(obj, self.obj_name)
235+ assert isinstance(value, TextPropertyValue)
236+ return value
237+
238+ def __set__(self, obj, new_text):
239+ assert isinstance(new_text, basestring)
240+
241+ if hasattr(obj, self.obj_name):
242+ old_value = self.__get__(obj)
243+ else:
244+ old_value = None
245+ new_value = TextPropertyValue(self, obj, new_text)
246+ setattr(obj, self.obj_name, new_value)
247+ obj.property_updated(self.name, old_value, new_value)
248+
249+
250+class TextPropertyValue(object):
251+ def __init__(self, parent, obj, text=''):
252+ self.parent = parent
253+ self.obj = obj
254+ self.text = text
255+
256+ def __str__(self):
257+ return self.text
258+
259+ def __repr__(self):
260+ return '<TextPropertyValue: {!r}>'.format(self.text)
261+
262+ def __nonzero__(self):
263+ return bool(self.text)
264+
265+ def __eq__(self, other):
266+ if isinstance(other, TextPropertyValue):
267+ return self.text == other.text
268+
269+ if isinstance(other, basestring):
270+ return self.text == other
271+
272+ raise ValueError
273+
274+ def prepend(self, other_text):
275+ """
276+ Prepend a string to the stored value
277+ """
278+ assert isinstance(other_text, basestring)
279+ self.text = other_text + self.text
280+ return self
281+
282+ def append(self, other_text):
283+ """
284+ Append a string to the stored value
285+ """
286+ assert isinstance(other_text, basestring)
287+ self.text = self.text + other_text
288+ return self
289+
290+
291+class ConfigurationSection(Section):
292+ """
293+ A configuration statement made of one or multiple lines
294+ """
295+ TRAILING_CHARS = string.whitespace + '\\'
296+
297+ owner = TextProperty('owner')
298+ qname = TextProperty('qname')
299+ qtype = TextProperty('qtype')
300+ value = TextProperty('value')
301+
302+ def __init__(self, parent, raw_lines):
303+ """
304+ Parse each element, so that it can be modified if needed
305+ """
306+ super(ConfigurationSection, self).__init__(parent)
307+ lines = [raw_lines[0].rstrip(self.TRAILING_CHARS)]
308+ for raw_line in raw_lines[1:]:
309+ lines.append(raw_line
310+ .lstrip()
311+ .rstrip(self.TRAILING_CHARS))
312+ text = ' '.join(lines)
313+ splitted_text = text.split(None, 3)
314+ self.owner = splitted_text[0]
315+ self.qname = splitted_text[1]
316+ self.qtype = splitted_text[2]
317+ if len(splitted_text) == 4:
318+ self.value = splitted_text[3]
319+ else:
320+ self.value = ''
321+
322+ def __str__(self):
323+ """
324+ Return text representation in a single line
325+ """
326+ if self.value:
327+ line = ('{} {} {} {}\n'
328+ .format(self.owner, self.qname, self.qtype,
329+ self.value))
330+ else:
331+ line = ('{} {} {}\n'
332+ .format(self.owner, self.qname, self.qtype))
333+ return line
334+
335+ def prepend(self, new_section):
336+ """
337+ Prepend a new section to this one
338+ """
339+ if isinstance(new_section, basestring):
340+ new_section = Section.new(self.parent, new_section)
341+
342+ assert isinstance(new_section, Section)
343+ self.parent.prepend(new_section, self)
344+
345+ def append(self, new_section):
346+ """
347+ Append a new section to this one
348+ """
349+ if isinstance(new_section, basestring):
350+ new_section = Section.new(self.parent, new_section)
351+
352+ assert isinstance(new_section, Section)
353+ self.parent.append(new_section, self)
354+
355+ def property_updated(self, property_name, old_value, new_value):
356+ self.parent.section_updated(self, property_name, old_value, new_value)
357
358=== modified file 'utah/provisioning/provisioning.py'
359--- utah/provisioning/provisioning.py 2012-08-29 18:46:24 +0000
360+++ utah/provisioning/provisioning.py 2012-09-06 12:53:21 +0000
361@@ -19,6 +19,7 @@
362 import apt.cache
363
364 from utah.iso import ISO
365+from utah.preseed import Preseed
366 from utah.provisioning.exceptions import UTAHProvisioningException
367 from utah.retry import retry
368 import utah.timeout
369@@ -772,57 +773,53 @@
370 self.logger.info('Setting up preseed')
371 if tmpdir is None:
372 tmpdir = self.tmpdir
373- preseed = open(os.path.join(tmpdir, 'initrd.d', 'preseed.cfg'), 'w')
374- for line in open(self.preseed, 'r'):
375- if 'preseed/late_command string' in line:
376- if self.installtype == 'desktop':
377- self.logger.info('Changing d-i latecommand '
378- + 'to ubiquity success_command '
379- + 'and prepending ubiquity lines')
380- line = line.replace('d-i', 'ubiquity')
381- line = line.replace('preseed/late_command string ',
382- ('ubiquity/success_command '
383- 'string chroot /target sh -c \'' # chroot start
384- 'export LOG_FILE=/var/log/utah-install; '
385- 'apt-get install -y --force-yes openssh-server '
386- '>>$LOG_FILE 2>&1 ; '
387- 'apt-get install -y --force-yes gdebi-core '
388- '>>$LOG_FILE 2>&1 ; '
389- '\'; '), # chroot end
390- 1)
391- line = ("ubiquity ubiquity/summary note\n"
392- "ubiquity ubiquity/reboot boolean true\n"
393- + line)
394- else:
395- self.logger.info('Prepending latecommand')
396- line = line.replace('preseed/late_command string ',
397- ('preseed/late_command string '
398- 'sh utah-latecommand ; '), 1)
399- line = line.rstrip(' ;')
400- if 'ubiquity/success_command string' in line:
401- self.logger.info('Prepending success_command')
402- line = line.replace('ubiquity/success_command string',
403- ('ubiquity/success_command string '
404- 'sh utah-latecommand ; '), 1)
405- line = line.rstrip(' ;')
406- if 'pkgsel/include' in line:
407- for pkgname in ('openssh-server', 'gdebi-core'):
408- if pkgname not in line:
409- self.logger.info('Adding {} to preseeded packages'
410- .format(pkgname))
411- line = line.replace('string ',
412- 'string {} '.format(pkgname))
413- line = line.rstrip() + "\n"
414- if 'netcfg/get_hostname' in line:
415- self.logger.info('Rewriting hostname to ' + self.name)
416- line = 'd-i netcfg/get_hostname string ' + self.name + "\n"
417- if 'passwd/username' in line:
418- self.logger.info('Rewriting username to ' + config.user)
419- line = 'd-i passwd/username string ' + config.user + "\n"
420- #Now that debug keeps the directory, this may not be needed
421- #self.logger.debug('Preseed line: ' + line)
422- preseed.write(line)
423- preseed.flush()
424+ preseed = Preseed(self.preseed)
425+ if 'preseed/late_command' in preseed:
426+ if self.installtype == 'desktop':
427+ self.logger.info('Changing d-i latecommand '
428+ + 'to ubiquity success_command '
429+ + 'and prepending ubiquity lines')
430+ question = preseed['preseed/late_command']
431+ question.owner = 'ubiquity'
432+ question.qname = 'ubiquity/success_command'
433+ question.value.prepend(
434+ 'chroot /target sh -c \'' # chroot start
435+ 'export LOG_FILE=/var/log/utah-install; '
436+ 'apt-get install -y --force-yes openssh-server '
437+ '>>$LOG_FILE 2>&1 ; '
438+ 'apt-get install -y --force-yes gdebi-core '
439+ '>>$LOG_FILE 2>&1 ; '
440+ '\'; ' # chroot end
441+ )
442+ question.prepend("ubiquity ubiquity/summary note")
443+ question.prepend("ubiquity ubiquity/reboot boolean true")
444+ else:
445+ self.logger.info('Prepending latecommand')
446+ question.value.prepend('sh utah-latecommand ; ')
447+ if 'ubiquity/success_command' in preseed:
448+ self.logger.info('Prepending success_command')
449+ question = preseed['ubiquity/success_command']
450+ question.value.prepend('sh utah-latecommand ; ')
451+ if 'pkgsel/include' in preseed:
452+ question = preseed['pkgsel/include']
453+ packages = question.value.text.split()
454+ for pkgname in ('openssh-server', 'gdebi-core'):
455+ if pkgname not in packages:
456+ self.logger.info('Adding {} to preseeded packages'
457+ .format(pkgname))
458+ packages.append(pkgname)
459+ question.value = ' '.join(packages)
460+
461+ if 'netcfg/get_hostname' in preseed:
462+ self.logger.info('Rewriting hostname to ' + self.name)
463+ question = preseed['netcfg/get_hostname']
464+ question.value = self.name
465+ if 'passwd/username' in preseed:
466+ self.logger.info('Rewriting username to ' + config.user)
467+ question = preseed['passwd/username']
468+ question.value = config.user
469+
470+ preseed.write(os.path.join(tmpdir, 'initrd.d', 'preseed.cfg'))
471
472 if self.installtype == 'desktop':
473 self.logger.info('Inserting preseed into casper')

Subscribers

People subscribed via source and target branches