Merge lp:~javier.collado/utah/preseed-parser into lp:utah
- preseed-parser
- Merge into dev
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Max Brustkern (community) | Approve | ||
Review via email:
|
Commit message
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.
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') |
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.