]> git.siccegge.de Git - forks/vmdebootstrap.git/blob - vmdebootstrap
pylint tweaks
[forks/vmdebootstrap.git] / vmdebootstrap
1 #! /usr/bin/python
2 # Copyright 2011-2013 Lars Wirzenius
3 # Copyright 2012 Codethink Limited
4 # Copyright 2014 Neil Williams <codehelp@debian.org>
5 #
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
19 import cliapp
20 import crypt
21 import logging
22 import os
23 import re
24 import shutil
25 import subprocess
26 import tempfile
27 import time
28
29
30 __version__ = '0.7'
31
32 # pylint: disable=invalid-name
33
34
35 class VmDebootstrap(cliapp.Application): # pylint: disable=too-many-public-methods
36
37 def __init__(self, progname=None, version=__version__, description=None, epilog=None):
38 super(VmDebootstrap, self).__init__(progname, version, description, epilog)
39 self.remove_dirs = []
40 self.mount_points = []
41
42 def add_settings(self):
43 default_arch = subprocess.check_output(
44 ["dpkg", "--print-architecture"]).strip()
45
46 self.settings.boolean(
47 ['verbose'], 'report what is going on')
48 self.settings.string(
49 ['image'], 'put created disk image in FILE',
50 metavar='FILE')
51 self.settings.bytesize(
52 ['size'],
53 'create a disk image of size SIZE (%default)',
54 metavar='SIZE',
55 default='1G')
56 self.settings.bytesize(
57 ['bootsize'],
58 'create boot partition of size SIZE (%default)',
59 metavar='BOOTSIZE',
60 default='0%')
61 self.settings.string(
62 ['boottype'],
63 'specify file system type for /boot/',
64 default='ext2')
65 self.settings.bytesize(
66 ['bootoffset'],
67 'Space to leave at start of the image for bootloader',
68 default='0')
69 self.settings.string(
70 ['part-type'],
71 'Partition type to use for this image',
72 default='msdos')
73 self.settings.string(
74 ['roottype'],
75 'specify file system type for /',
76 default='ext4')
77 self.settings.bytesize(
78 ['swap'],
79 'create swap space of size SIZE (min 256Mb)')
80 self.settings.string(
81 ['foreign'],
82 'set up foreign debootstrap environment using provided program (ie binfmt handler)')
83 self.settings.string(
84 ['variant'],
85 'select debootstrap variant it not using the default')
86 self.settings.boolean(
87 ['extlinux'],
88 'install extlinux?',
89 default=True)
90 self.settings.string(
91 ['tarball'],
92 "tar up the disk's contents in FILE",
93 metavar='FILE')
94 self.settings.string(
95 ['apt-mirror'],
96 'configure apt to use MIRROR',
97 metavar='URL')
98 self.settings.string(
99 ['mirror'],
100 'use MIRROR as package source (%default)',
101 metavar='URL',
102 default='http://http.debian.net/debian/')
103 self.settings.string(
104 ['arch'],
105 'architecture to use (%default)',
106 metavar='ARCH',
107 default=default_arch)
108 self.settings.string(
109 ['distribution'],
110 'release to use (%default)',
111 metavar='NAME',
112 default='stable')
113 self.settings.string_list(
114 ['package'],
115 'install PACKAGE onto system')
116 self.settings.string_list(
117 ['custom-package'],
118 'install package in DEB file onto system (not from mirror)',
119 metavar='DEB')
120 self.settings.boolean(
121 ['no-kernel'],
122 'do not install a linux package')
123 self.settings.boolean(
124 ['enable-dhcp'],
125 'enable DHCP on eth0')
126 self.settings.string(
127 ['root-password'],
128 'set root password',
129 metavar='PASSWORD')
130 self.settings.boolean(
131 ['lock-root-password'],
132 'lock root account so they cannot login?')
133 self.settings.string(
134 ['customize'],
135 'run SCRIPT after setting up system',
136 metavar='SCRIPT')
137 self.settings.string(
138 ['hostname'],
139 'set name to HOSTNAME (%default)',
140 metavar='HOSTNAME',
141 default='debian')
142 self.settings.string_list(
143 ['user'],
144 'create USER with PASSWORD',
145 metavar='USER/PASSWORD')
146 self.settings.boolean(
147 ['serial-console'],
148 'configure image to use a serial console')
149 self.settings.string(
150 ['serial-console-command'],
151 'command to manage the serial console, appended to /etc/inittab (%default)',
152 metavar='COMMAND',
153 default='/sbin/getty -L ttyS0 115200 vt100')
154 self.settings.boolean(
155 ['sudo'],
156 'install sudo, and if user is created, add them to sudo group')
157 self.settings.string(
158 ['owner'],
159 'the user who will own the image when the build is complete.')
160 self.settings.boolean(
161 ['squash'],
162 'use squashfs on the final image.')
163 self.settings.boolean(
164 ['configure-apt'],
165 'Create an apt source based on the distribution and mirror selected.')
166 self.settings.boolean(
167 ['mbr'],
168 'Run install-mbr (default if extlinux used)')
169 self.settings.boolean(
170 ['grub'],
171 'Install and configure grub2 - disables extlinux.')
172 self.settings.boolean(
173 ['sparse'],
174 'Do not fill the image with zeros to keep a sparse disk image',
175 default=False)
176 self.settings.boolean(
177 ['pkglist'],
178 'Create a list of package names included in the image.')
179
180 def process_args(self, args): # pylint: disable=too-many-branches,too-many-statements
181 if not self.settings['image'] and not self.settings['tarball']:
182 raise cliapp.AppException(
183 'You must give disk image filename, or tarball filename')
184 if self.settings['image'] and not self.settings['size']:
185 raise cliapp.AppException(
186 'If disk image is specified, you must give image size.')
187
188 rootdir = None
189 try:
190 rootdev = None
191 roottype = self.settings['roottype']
192 bootdev = None
193 boottype = None
194 if self.settings['image']:
195 self.create_empty_image()
196 self.partition_image()
197 if self.settings['mbr'] or self.settings['extlinux']:
198 self.install_mbr()
199 (rootdev, bootdev, swapdev) = self.setup_kpartx()
200 if self.settings['swap'] > 0:
201 self.message("Creating swap space")
202 self.runcmd(['mkswap', swapdev])
203 self.mkfs(rootdev, fstype=roottype)
204 rootdir = self.mount(rootdev)
205 if bootdev:
206 if self.settings['boottype']:
207 boottype = self.settings['boottype']
208 else:
209 boottype = 'ext2'
210 self.mkfs(bootdev, fstype=boottype)
211 bootdir = '%s/%s' % (rootdir, 'boot/')
212 os.mkdir(bootdir)
213 self.mount(bootdev, bootdir)
214 else:
215 rootdir = self.mkdtemp()
216 self.debootstrap(rootdir)
217 self.set_hostname(rootdir)
218 self.create_fstab(rootdir, rootdev, roottype, bootdev, boottype)
219 self.install_debs(rootdir)
220 self.cleanup_apt_cache(rootdir)
221 self.set_root_password(rootdir)
222 self.create_users(rootdir)
223 self.remove_udev_persistent_rules(rootdir)
224 self.setup_networking(rootdir)
225 if self.settings['configure-apt'] or self.settings['apt-mirror']:
226 self.configure_apt(rootdir)
227 self.customize(rootdir)
228 self.update_initramfs(rootdir)
229
230 if self.settings['image']:
231 if self.settings['grub']:
232 self.install_grub2(rootdev, rootdir)
233 elif self.settings['extlinux']:
234 self.install_extlinux(rootdev, rootdir)
235 self.append_serial_console(rootdir)
236 self.optimize_image(rootdir)
237 if self.settings['squash']:
238 self.squash()
239 if self.settings['pkglist']:
240 self.list_installed_pkgs(rootdir)
241
242 if self.settings['foreign']:
243 os.unlink('%s/usr/bin/%s' %
244 (rootdir, os.path.basename(self.settings['foreign'])))
245
246 if self.settings['tarball']:
247 self.create_tarball(rootdir)
248
249 if self.settings['owner']:
250 self.chown()
251 except BaseException as e:
252 self.message('EEEK! Something bad happened...')
253 if rootdir:
254 db_log = os.path.join(rootdir, 'debootstrap', 'debootstrap.log')
255 if os.path.exists(db_log):
256 shutil.copy(db_log, os.getcwd())
257 self.message(e)
258 self.cleanup_system()
259 raise
260 else:
261 self.cleanup_system()
262
263 def message(self, msg):
264 logging.info(msg)
265 if self.settings['verbose']:
266 print msg
267
268 def runcmd(self, argv, stdin='', ignore_fail=False, env=None, **kwargs):
269 logging.debug('runcmd: %s %s %s', argv, env, kwargs)
270 p = subprocess.Popen(argv, stdin=subprocess.PIPE,
271 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
272 env=env, **kwargs)
273 out, err = p.communicate(stdin)
274 if p.returncode != 0:
275 msg = 'command failed: %s\n%s\n%s' % (argv, out, err)
276 logging.error(msg)
277 if not ignore_fail:
278 raise cliapp.AppException(msg)
279 return out
280
281 def mkdtemp(self):
282 dirname = tempfile.mkdtemp()
283 self.remove_dirs.append(dirname)
284 logging.debug('mkdir %s', dirname)
285 return dirname
286
287 def mount(self, device, path=None):
288 if not path:
289 mount_point = self.mkdtemp()
290 else:
291 mount_point = path
292 self.message('Mounting %s on %s' % (device, mount_point))
293 self.runcmd(['mount', device, mount_point])
294 self.mount_points.append(mount_point)
295 logging.debug('mounted %s on %s', device, mount_point)
296 return mount_point
297
298 def create_empty_image(self):
299 self.message('Creating disk image')
300 self.runcmd(['qemu-img', 'create', '-f', 'raw',
301 self.settings['image'],
302 str(self.settings['size'])])
303
304 def partition_image(self):
305 """
306 Uses fat16 (msdos) partitioning by default, use part-type to change.
307 If bootoffset is specified, the first actual partition
308 starts at that offset to allow customisation scripts to
309 put bootloader images into the space, e.g. u-boot.
310 """
311 self.message('Creating partitions')
312 self.runcmd(['parted', '-s', self.settings['image'],
313 'mklabel', self.settings['part-type']])
314 partoffset = 0
315 bootsize = 0
316 extent = '100%'
317 swap = 256 * 1024 * 1024
318 if self.settings['swap'] > 0:
319 if self.settings['swap'] > swap:
320 swap = self.settings['swap']
321 else:
322 # minimum 256Mb as default qemu ram is 128Mb
323 logging.debug("Setting minimum 256Mb swap space")
324 extent = "%s%%" % int(100 * (self.settings['size'] - swap) / self.settings['size'])
325 if self.settings['bootoffset'] and self.settings['bootoffset'] is not '0':
326 # turn v.small offsets into something at least possible to create.
327 if self.settings['bootoffset'] < 1048576:
328 partoffset = 1
329 logging.info(
330 "Setting bootoffset %smib to allow for %s bytes",
331 partoffset, self.settings['bootoffset'])
332 else:
333 partoffset = self.settings['bootoffset'] / (1024 * 1024)
334 self.message("Using bootoffset: %smib %s bytes" % (partoffset, self.settings['bootoffset']))
335 if self.settings['bootsize'] and self.settings['bootsize'] is not '0%':
336 if self.settings['grub'] and not partoffset:
337 partoffset = 1
338 bootsize = self.settings['bootsize'] / (1024 * 1024)
339 bootsize += partoffset
340 self.message("Using bootsize %smib: %s bytes" % (bootsize, self.settings['bootsize']))
341 logging.debug("Starting boot partition at %sMb", bootsize)
342 self.runcmd(['parted', '-s', self.settings['image'],
343 'mkpart', 'primary', 'fat16', str(partoffset), str(bootsize)])
344 logging.debug("Starting root partition at %sMb", partoffset)
345 self.runcmd(['parted', '-s', self.settings['image'],
346 'mkpart', 'primary', str(bootsize), extent])
347 else:
348 self.runcmd(['parted', '-s', self.settings['image'],
349 'mkpart', 'primary', '0%', extent])
350 self.runcmd(['parted', '-s', self.settings['image'],
351 'set', '1', 'boot', 'on'])
352 if self.settings['swap'] > 0:
353 logging.debug("Creating swap partition")
354 self.runcmd(['parted', '-s', self.settings['image'],
355 'mkpart', 'primary', 'linux-swap', extent, '100%'])
356
357 def update_initramfs(self, rootdir):
358 cmd = os.path.join('usr', 'sbin', 'update-initramfs')
359 if os.path.exists(os.path.join(rootdir, cmd)):
360 self.message("Updating the initramfs")
361 self.runcmd(['chroot', rootdir, cmd, '-u'])
362
363 def install_mbr(self):
364 if os.path.exists("/sbin/install-mbr"):
365 self.message('Installing MBR')
366 self.runcmd(['install-mbr', self.settings['image']])
367 else:
368 msg = "mbr enabled but /sbin/install-mbr not found" \
369 " - please install the mbr package."
370 raise cliapp.AppException(msg)
371
372 def setup_kpartx(self):
373 bootindex = None
374 swapindex = None
375 out = self.runcmd(['kpartx', '-avs', self.settings['image']])
376 if self.settings['bootsize'] and self.settings['swap'] > 0:
377 bootindex = 0
378 rootindex = 1
379 swapindex = 2
380 parts = 3
381 elif self.settings['bootsize']:
382 bootindex = 0
383 rootindex = 1
384 parts = 2
385 elif self.settings['swap'] > 0:
386 rootindex = 0
387 swapindex = 1
388 parts = 2
389 else:
390 rootindex = 0
391 parts = 1
392 boot = None
393 swap = None
394 devices = [line.split()[2]
395 for line in out.splitlines()
396 if line.startswith('add map ')]
397 if len(devices) != parts:
398 msg = 'Surprising number of partitions - check output of losetup -a'
399 logging.debug("%s", self.runcmd(['losetup', '-a']))
400 logging.debug("%s: devices=%s parts=%s", msg, devices, parts)
401 raise cliapp.AppException(msg)
402 root = '/dev/mapper/%s' % devices[rootindex]
403 if self.settings['bootsize']:
404 boot = '/dev/mapper/%s' % devices[bootindex]
405 if self.settings['swap'] > 0:
406 swap = '/dev/mapper/%s' % devices[swapindex]
407 return root, boot, swap
408
409 def mkfs(self, device, fstype):
410 self.message('Creating filesystem %s' % fstype)
411 self.runcmd(['mkfs', '-t', fstype, device])
412
413 def debootstrap(self, rootdir):
414 msg = "(%s)" % self.settings['variant'] if self.settings['variant'] else ''
415 self.message('Debootstrapping %s %s' % (self.settings['distribution'], msg))
416
417 include = self.settings['package']
418
419 if not self.settings['foreign']:
420 include.append('acpid')
421
422 if self.settings['grub']:
423 include.append('grub-pc')
424
425 if not self.settings['no-kernel']:
426 if self.settings['arch'] == 'i386':
427 kernel_arch = '486'
428 elif self.settings['arch'] == 'armhf':
429 kernel_arch = 'armmp'
430 else:
431 kernel_arch = self.settings['arch']
432 kernel_image = 'linux-image-%s' % kernel_arch
433 include.append(kernel_image)
434
435 if self.settings['sudo'] and 'sudo' not in include:
436 include.append('sudo')
437
438 args = ['debootstrap', '--arch=%s' % self.settings['arch']]
439
440 if self.settings['package']:
441 args.append(
442 '--include=%s' % ','.join(include))
443 if self.settings['foreign']:
444 args.append('--foreign')
445 if self.settings['variant']:
446 args.append('--variant')
447 args.append(self.settings['variant'])
448 args += [self.settings['distribution'],
449 rootdir, self.settings['mirror']]
450 logging.debug(" ".join(args))
451 self.runcmd(args)
452 if self.settings['foreign']:
453 # set a noninteractive debconf environment for secondstage
454 env = {
455 "DEBIAN_FRONTEND": "noninteractive",
456 "DEBCONF_NONINTERACTIVE_SEEN": "true",
457 "LC_ALL": "C"
458 }
459 # add the mapping to the complete environment.
460 env.update(os.environ)
461 # First copy the binfmt handler over
462 self.message('Setting up binfmt handler')
463 shutil.copy(self.settings['foreign'], '%s/usr/bin/' % rootdir)
464 # Next, run the package install scripts etc.
465 self.message('Running debootstrap second stage')
466 self.runcmd(['chroot', rootdir,
467 '/debootstrap/debootstrap', '--second-stage'],
468 env=env)
469
470 def set_hostname(self, rootdir):
471 hostname = self.settings['hostname']
472 with open(os.path.join(rootdir, 'etc', 'hostname'), 'w') as f:
473 f.write('%s\n' % hostname)
474
475 etc_hosts = os.path.join(rootdir, 'etc', 'hosts')
476 try:
477 with open(etc_hosts, 'r') as f:
478 data = f.read()
479 with open(etc_hosts, 'w') as f:
480 for line in data.splitlines():
481 if line.startswith('127.0.0.1'):
482 line += ' %s' % hostname
483 f.write('%s\n' % line)
484 except IOError:
485 pass
486
487 def create_fstab(self, rootdir, rootdev, roottype, bootdev, boottype): # pylint: disable=too-many-arguments
488 def fsuuid(device):
489 out = self.runcmd(['blkid', '-c', '/dev/null', '-o', 'value',
490 '-s', 'UUID', device])
491 return out.splitlines()[0].strip()
492
493 if rootdev:
494 rootdevstr = 'UUID=%s' % fsuuid(rootdev)
495 else:
496 rootdevstr = '/dev/sda1'
497
498 if bootdev:
499 bootdevstr = 'UUID=%s' % fsuuid(bootdev)
500 else:
501 bootdevstr = None
502
503 fstab = os.path.join(rootdir, 'etc', 'fstab')
504 with open(fstab, 'w') as f:
505 f.write('proc /proc proc defaults 0 0\n')
506 f.write('%s / %s errors=remount-ro 0 1\n' % (rootdevstr, roottype))
507 if bootdevstr:
508 f.write('%s /boot %s errors=remount-ro 0 2\n' % (bootdevstr, boottype))
509 if self.settings['swap'] > 0:
510 f.write("/dev/sda3 swap swap defaults 0 0\n")
511 elif self.settings['swap'] > 0:
512 f.write("/dev/sda2 swap swap defaults 0 0\n")
513
514 def install_debs(self, rootdir):
515 if not self.settings['custom-package']:
516 return
517 self.message('Installing custom packages')
518 tmp = os.path.join(rootdir, 'tmp', 'install_debs')
519 os.mkdir(tmp)
520 for deb in self.settings['custom-package']:
521 shutil.copy(deb, tmp)
522 filenames = [os.path.join('/tmp/install_debs', os.path.basename(deb))
523 for deb in self.settings['custom-package']]
524 out, err, _ = \
525 self.runcmd_unchecked(['chroot', rootdir, 'dpkg', '-i'] + filenames)
526 logging.debug('stdout:\n%s', out)
527 logging.debug('stderr:\n%s', err)
528 out = self.runcmd(['chroot', rootdir,
529 'apt-get', '-f', '--no-remove', 'install'])
530 logging.debug('stdout:\n%s', out)
531 shutil.rmtree(tmp)
532
533 def cleanup_apt_cache(self, rootdir):
534 out = self.runcmd(['chroot', rootdir, 'apt-get', 'clean'])
535 logging.debug('stdout:\n%s', out)
536
537 def set_root_password(self, rootdir):
538 if self.settings['root-password']:
539 self.message('Setting root password')
540 self.set_password(rootdir, 'root', self.settings['root-password'])
541 elif self.settings['lock-root-password']:
542 self.message('Locking root password')
543 self.runcmd(['chroot', rootdir, 'passwd', '-l', 'root'])
544 else:
545 self.message('Give root an empty password')
546 self.delete_password(rootdir, 'root')
547
548 def create_users(self, rootdir):
549 def create_user(user):
550 self.runcmd(['chroot', rootdir, 'adduser', '--gecos', user,
551 '--disabled-password', user])
552 if self.settings['sudo']:
553 self.runcmd(['chroot', rootdir, 'adduser', user, 'sudo'])
554
555 for userpass in self.settings['user']:
556 if '/' in userpass:
557 user, password = userpass.split('/', 1)
558 create_user(user)
559 self.set_password(rootdir, user, password)
560 else:
561 create_user(userpass)
562 self.delete_password(rootdir, userpass)
563
564 def set_password(self, rootdir, user, password):
565 encrypted = crypt.crypt(password, '..')
566 self.runcmd(['chroot', rootdir, 'usermod', '-p', encrypted, user])
567
568 def delete_password(self, rootdir, user):
569 self.runcmd(['chroot', rootdir, 'passwd', '-d', user])
570
571 def remove_udev_persistent_rules(self, rootdir):
572 self.message('Removing udev persistent cd and net rules')
573 for x in ['70-persistent-cd.rules', '70-persistent-net.rules']:
574 pathname = os.path.join(rootdir, 'etc', 'udev', 'rules.d', x)
575 if os.path.exists(pathname):
576 logging.debug('rm %s', pathname)
577 os.remove(pathname)
578 else:
579 logging.debug('not removing non-existent %s', pathname)
580
581 def setup_networking(self, rootdir):
582 self.message('Setting up networking')
583
584 f = open(os.path.join(rootdir, 'etc', 'network', 'interfaces'), 'w')
585 f.write('auto lo\n')
586 f.write('iface lo inet loopback\n')
587
588 if self.settings['enable-dhcp']:
589 f.write('\n')
590 f.write('auto eth0\n')
591 f.write('iface eth0 inet dhcp\n')
592
593 f.close()
594
595 def append_serial_console(self, rootdir):
596 if self.settings['serial-console']:
597 serial_command = self.settings['serial-console-command']
598 logging.debug('adding getty to serial console')
599 inittab = os.path.join(rootdir, 'etc/inittab')
600 # to autologin, serial_command can contain '-a root'
601 with open(inittab, 'a') as f:
602 f.write('\nS0:23:respawn:%s\n' % serial_command)
603
604 # pylint: disable=no-self-use
605 def _grub_serial_console(self, rootdir):
606 cmdline = 'GRUB_CMDLINE_LINUX_DEFAULT="console=tty0 console=tty1 console=ttyS0,38400n8"'
607 terminal = 'GRUB_TERMINAL="serial gfxterm"'
608 command = 'GRUB_SERIAL_COMMAND="serial --speed=38400 --unit=0 --parity=no --stop=1"'
609 grub_cfg = os.path.join(rootdir, 'etc', 'default', 'grub')
610 logging.debug("Allowing serial output in grub config %s", grub_cfg)
611 with open(grub_cfg, 'a+') as cfg:
612 cfg.write("# %s serial support\n" % os.path.basename(__file__))
613 cfg.write("%s\n" % cmdline)
614 cfg.write("%s\n" % terminal)
615 cfg.write("%s\n" % command)
616
617 def install_grub2(self, rootdev, rootdir):
618 self.message("Configuring grub2")
619 # rely on kpartx using consistent naming to map loop0p1 to loop0
620 install_dev = os.path.join('/dev', os.path.basename(rootdev)[:-2])
621 self.runcmd(['mount', '/dev', '-t', 'devfs', '-obind',
622 '%s' % os.path.join(rootdir, 'dev')])
623 self.runcmd(['mount', '/proc', '-t', 'proc', '-obind',
624 '%s' % os.path.join(rootdir, 'proc')])
625 self.runcmd(['mount', '/sys', '-t', 'sysfs', '-obind',
626 '%s' % os.path.join(rootdir, 'sys')])
627 if self.settings['serial-console']:
628 self._grub_serial_console(rootdir)
629
630 try:
631 self.runcmd(['chroot', rootdir, 'update-grub'])
632 self.runcmd(['chroot', rootdir, 'grub-install', install_dev])
633 except cliapp.AppException:
634 self.message("Failed. Is grub2-common installed? Using extlinux.")
635 self.install_extlinux(rootdev, rootdir)
636 self.runcmd(['umount', os.path.join(rootdir, 'sys')])
637 self.runcmd(['umount', os.path.join(rootdir, 'proc')])
638 self.runcmd(['umount', os.path.join(rootdir, 'dev')])
639
640 def install_extlinux(self, rootdev, rootdir):
641 if not os.path.exists("/usr/bin/extlinux"):
642 self.message("extlinux not installed, skipping.")
643 return
644 self.message('Installing extlinux')
645
646 def find(pattern):
647 dirname = os.path.join(rootdir, 'boot')
648 basenames = os.listdir(dirname)
649 logging.debug('find: %s', basenames)
650 for basename in basenames:
651 if re.search(pattern, basename):
652 return os.path.join('boot', basename)
653 raise cliapp.AppException('Cannot find match: %s' % pattern)
654
655 try:
656 kernel_image = find('vmlinuz-.*')
657 initrd_image = find('initrd.img-.*')
658 except cliapp.AppException as e:
659 self.message("Unable to find kernel. Not installing extlinux.")
660 logging.debug("No kernel found. %s. Skipping install of extlinux.", e)
661 return
662
663 out = self.runcmd(['blkid', '-c', '/dev/null', '-o', 'value',
664 '-s', 'UUID', rootdev])
665 uuid = out.splitlines()[0].strip()
666
667 conf = os.path.join(rootdir, 'extlinux.conf')
668 logging.debug('configure extlinux %s', conf)
669 kserial = 'console=ttyS0,115200' if self.settings['serial-console'] else ''
670 extserial = 'serial 0 115200' if self.settings['serial-console'] else ''
671 msg = '''
672 default linux
673 timeout 1
674
675 label linux
676 kernel %(kernel)s
677 append initrd=%(initrd)s root=UUID=%(uuid)s ro %(kserial)s
678 %(extserial)s
679 ''' % {
680 'kernel': kernel_image, # pylint: disable=bad-continuation
681 'initrd': initrd_image, # pylint: disable=bad-continuation
682 'uuid': uuid, # pylint: disable=bad-continuation
683 'kserial': kserial, # pylint: disable=bad-continuation
684 'extserial': extserial, # pylint: disable=bad-continuation
685 } # pylint: disable=bad-continuation
686 logging.debug("extlinux config:\n%s", msg)
687
688 # python multiline string substitution is just ugly.
689 # use an external file or live with the mangling, no point in
690 # mangling the string to remove spaces just to keep it pretty in source.
691 f = open(conf, 'w')
692 f.write(msg)
693
694 self.runcmd(['extlinux', '--install', rootdir])
695 self.runcmd(['sync'])
696 time.sleep(2)
697
698 def optimize_image(self, rootdir):
699 """
700 Filing up the image with zeros will increase its compression rate
701 """
702 if not self.settings['sparse']:
703 zeros = os.path.join(rootdir, 'ZEROS')
704 self.runcmd_unchecked(['dd', 'if=/dev/zero', 'of=' + zeros, 'bs=1M'])
705 self.runcmd(['rm', '-f', zeros])
706
707 def squash(self):
708 """
709 Run squashfs on the image.
710 """
711 if not os.path.exists('/usr/bin/mksquashfs'):
712 logging.warning("Squash selected but mksquashfs not found!")
713 return
714 self.message("Running mksquashfs")
715 suffixed = "%s.squashfs" % self.settings['image']
716 self.runcmd(['mksquashfs', self.settings['image'],
717 suffixed,
718 '-no-progress', '-comp', 'xz'], ignore_fail=False)
719 os.unlink(self.settings['image'])
720 self.settings['image'] = suffixed
721
722 def cleanup_system(self):
723 # Clean up after any errors.
724
725 self.message('Cleaning up')
726
727 # Umount in the reverse mount order
728 if self.settings['image']:
729 for i in range(len(self.mount_points) - 1, -1, -1):
730 mount_point = self.mount_points[i]
731 try:
732 self.runcmd(['umount', mount_point], ignore_fail=False)
733 except cliapp.AppException:
734 logging.debug("umount failed, sleeping and trying again")
735 time.sleep(5)
736 self.runcmd(['umount', mount_point], ignore_fail=False)
737
738 self.runcmd(['kpartx', '-d', self.settings['image']], ignore_fail=True)
739
740 for dirname in self.remove_dirs:
741 shutil.rmtree(dirname)
742
743 def customize(self, rootdir):
744 script = self.settings['customize']
745 if not script:
746 return
747 if not os.path.exists(script):
748 example = os.path.join("/usr/share/vmdebootstrap/examples/", script)
749 if not os.path.exists(example):
750 self.message("Unable to find %s" % script)
751 return
752 script = example
753 self.message('Running customize script %s' % script)
754 logging.info("rootdir=%s image=%s", rootdir, self.settings['image'])
755 with open('/dev/tty', 'w') as tty:
756 try:
757 cliapp.runcmd([script, rootdir, self.settings['image']], stdout=tty, stderr=tty)
758 except IOError:
759 subprocess.call([script, rootdir, self.settings['image']])
760
761 def create_tarball(self, rootdir):
762 # Create a tarball of the disk's contents
763 # shell out to runcmd since it more easily handles rootdir
764 self.message('Creating tarball of disk contents')
765 self.runcmd(['tar', '-cf', self.settings['tarball'], '-C', rootdir, '.'])
766
767 def chown(self):
768 # Change image owner after completed build
769 if self.settings['image']:
770 filename = self.settings['image']
771 elif self.settings['tarball']:
772 filename = self.settings['tarball']
773 else:
774 return
775 self.message("Changing owner to %s" % self.settings["owner"])
776 subprocess.call(["chown", self.settings["owner"], filename])
777
778 def list_installed_pkgs(self, rootdir):
779 # output the list of installed packages for sources identification
780 self.message("Creating a list of installed binary package names")
781 out = self.runcmd(['chroot', rootdir,
782 'dpkg-query', '-W', "-f='${Package}.deb\n'"])
783 with open('dpkg.list', 'w') as dpkg:
784 dpkg.write(out)
785
786 def configure_apt(self, rootdir):
787 # use the distribution and mirror to create an apt source
788 self.message("Configuring apt to use distribution and mirror")
789 conf = os.path.join(rootdir, 'etc', 'apt', 'sources.list.d', 'base.list')
790 logging.debug('configure apt %s', conf)
791 mirror = self.settings['mirror']
792 if self.settings['apt-mirror']:
793 mirror = self.settings['apt-mirror']
794 self.message("Setting apt mirror to %s" % mirror)
795 os.unlink(os.path.join(rootdir, 'etc', 'apt', 'sources.list'))
796 f = open(conf, 'w')
797 line = 'deb %s %s main\n' % (mirror, self.settings['distribution'])
798 f.write(line)
799 line = '#deb-src %s %s main\n' % (mirror, self.settings['distribution'])
800 f.write(line)
801 f.close()
802 # ensure the apt sources have valid lists
803 self.runcmd(['chroot', rootdir, 'apt-get', '-qq', 'update'])
804
805 if __name__ == '__main__':
806 VmDebootstrap(version=__version__).run()