]> git.siccegge.de Git - forks/vmdebootstrap.git/blob - vmdebootstrap
Ensure all tests for swap look for a number
[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.6'
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, 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 bootsize = self.settings['bootsize'] / (1024 * 1024)
337 bootsize += partoffset
338 self.message("Using bootsize %smib: %s bytes" % (bootsize, self.settings['bootsize']))
339 logging.debug("Starting boot partition at %sMb", bootsize)
340 self.runcmd(['parted', '-s', self.settings['image'],
341 'mkpart', 'primary', 'fat16', str(partoffset), str(bootsize)])
342 logging.debug("Starting root partition at %sMb", partoffset)
343 self.runcmd(['parted', '-s', self.settings['image'],
344 'mkpart', 'primary', str(bootsize), extent])
345 else:
346 self.runcmd(['parted', '-s', self.settings['image'],
347 'mkpart', 'primary', '0%', extent])
348 self.runcmd(['parted', '-s', self.settings['image'],
349 'set', '1', 'boot', 'on'])
350 if self.settings['swap'] > 0:
351 logging.debug("Creating swap partition")
352 self.runcmd(['parted', '-s', self.settings['image'],
353 'mkpart', 'primary', 'linux-swap', extent, '100%'])
354
355 def update_initramfs(self, rootdir):
356 cmd = os.path.join('usr', 'sbin', 'update-initramfs')
357 if os.path.exists(os.path.join(rootdir, cmd)):
358 self.message("Updating the initramfs")
359 self.runcmd(['chroot', rootdir, cmd, '-u'])
360
361 def install_mbr(self):
362 if os.path.exists("/sbin/install-mbr"):
363 self.message('Installing MBR')
364 self.runcmd(['install-mbr', self.settings['image']])
365 else:
366 msg = "mbr enabled but /sbin/install-mbr not found" \
367 " - please install the mbr package."
368 raise cliapp.AppException(msg)
369
370 def setup_kpartx(self):
371 bootindex = None
372 swapindex = None
373 out = self.runcmd(['kpartx', '-avs', self.settings['image']])
374 if self.settings['bootsize'] and self.settings['swap'] > 0:
375 bootindex = 0
376 rootindex = 1
377 swapindex = 2
378 parts = 3
379 elif self.settings['bootsize']:
380 bootindex = 0
381 rootindex = 1
382 parts = 2
383 elif self.settings['swap'] > 0:
384 rootindex = 0
385 swapindex = 1
386 parts = 2
387 else:
388 rootindex = 0
389 parts = 1
390 boot = None
391 swap = None
392 devices = [line.split()[2]
393 for line in out.splitlines()
394 if line.startswith('add map ')]
395 if len(devices) != parts:
396 msg = 'Surprising number of partitions - check output of losetup -a'
397 logging.debug("%s" % self.runcmd(['losetup', '-a']))
398 logging.debug("%s: devices=%s parts=%s", msg, devices, parts)
399 raise cliapp.AppException(msg)
400 root = '/dev/mapper/%s' % devices[rootindex]
401 if self.settings['bootsize']:
402 boot = '/dev/mapper/%s' % devices[bootindex]
403 if self.settings['swap'] > 0:
404 swap = '/dev/mapper/%s' % devices[swapindex]
405 return root, boot, swap
406
407 def mkfs(self, device, fstype):
408 self.message('Creating filesystem %s' % fstype)
409 self.runcmd(['mkfs', '-t', fstype, device])
410
411 def debootstrap(self, rootdir):
412 msg = "(%s)" % self.settings['variant'] if self.settings['variant'] else ''
413 self.message('Debootstrapping %s %s' % (self.settings['distribution'], msg))
414
415 include = self.settings['package']
416
417 if not self.settings['foreign']:
418 include.append('acpid')
419
420 if self.settings['grub']:
421 include.append('grub2')
422
423 if not self.settings['no-kernel']:
424 if self.settings['arch'] == 'i386':
425 kernel_arch = '486'
426 else:
427 kernel_arch = self.settings['arch']
428 kernel_image = 'linux-image-%s' % kernel_arch
429 include.append(kernel_image)
430
431 if self.settings['sudo'] and 'sudo' not in include:
432 include.append('sudo')
433
434 args = ['debootstrap', '--arch=%s' % self.settings['arch']]
435
436 if self.settings['package']:
437 args.append(
438 '--include=%s' % ','.join(include))
439 if self.settings['foreign']:
440 args.append('--foreign')
441 if self.settings['variant']:
442 args.append('--variant')
443 args.append(self.settings['variant'])
444 args += [self.settings['distribution'],
445 rootdir, self.settings['mirror']]
446 logging.debug(" ".join(args))
447 self.runcmd(args)
448 if self.settings['foreign']:
449 # set a noninteractive debconf environment for secondstage
450 env = {
451 "DEBIAN_FRONTEND": "noninteractive",
452 "DEBCONF_NONINTERACTIVE_SEEN": "true",
453 "LC_ALL": "C"
454 }
455 # add the mapping to the complete environment.
456 env.update(os.environ)
457 # First copy the binfmt handler over
458 self.message('Setting up binfmt handler')
459 shutil.copy(self.settings['foreign'], '%s/usr/bin/' % rootdir)
460 # Next, run the package install scripts etc.
461 self.message('Running debootstrap second stage')
462 self.runcmd(['chroot', rootdir,
463 '/debootstrap/debootstrap', '--second-stage'],
464 env=env)
465
466 def set_hostname(self, rootdir):
467 hostname = self.settings['hostname']
468 with open(os.path.join(rootdir, 'etc', 'hostname'), 'w') as f:
469 f.write('%s\n' % hostname)
470
471 etc_hosts = os.path.join(rootdir, 'etc', 'hosts')
472 try:
473 with open(etc_hosts, 'r') as f:
474 data = f.read()
475 with open(etc_hosts, 'w') as f:
476 for line in data.splitlines():
477 if line.startswith('127.0.0.1'):
478 line += ' %s' % hostname
479 f.write('%s\n' % line)
480 except IOError:
481 pass
482
483 def create_fstab(self, rootdir, rootdev, roottype, bootdev, boottype): # pylint: disable=too-many-arguments
484 def fsuuid(device):
485 out = self.runcmd(['blkid', '-c', '/dev/null', '-o', 'value',
486 '-s', 'UUID', device])
487 return out.splitlines()[0].strip()
488
489 if rootdev:
490 rootdevstr = 'UUID=%s' % fsuuid(rootdev)
491 else:
492 rootdevstr = '/dev/sda1'
493
494 if bootdev:
495 bootdevstr = 'UUID=%s' % fsuuid(bootdev)
496 else:
497 bootdevstr = None
498
499 fstab = os.path.join(rootdir, 'etc', 'fstab')
500 with open(fstab, 'w') as f:
501 f.write('proc /proc proc defaults 0 0\n')
502 f.write('%s / %s errors=remount-ro 0 1\n' % (rootdevstr, roottype))
503 if bootdevstr:
504 f.write('%s /boot %s errors=remount-ro 0 2\n' % (bootdevstr, boottype))
505 if self.settings['swap'] > 0:
506 f.write("/dev/sda3 swap swap defaults 0 0\n")
507 elif self.settings['swap'] > 0:
508 f.write("/dev/sda2 swap swap defaults 0 0\n")
509
510 def install_debs(self, rootdir):
511 if not self.settings['custom-package']:
512 return
513 self.message('Installing custom packages')
514 tmp = os.path.join(rootdir, 'tmp', 'install_debs')
515 os.mkdir(tmp)
516 for deb in self.settings['custom-package']:
517 shutil.copy(deb, tmp)
518 filenames = [os.path.join('/tmp/install_debs', os.path.basename(deb))
519 for deb in self.settings['custom-package']]
520 out, err, _ = \
521 self.runcmd_unchecked(['chroot', rootdir, 'dpkg', '-i'] + filenames)
522 logging.debug('stdout:\n%s', out)
523 logging.debug('stderr:\n%s', err)
524 out = self.runcmd(['chroot', rootdir,
525 'apt-get', '-f', '--no-remove', 'install'])
526 logging.debug('stdout:\n%s', out)
527 shutil.rmtree(tmp)
528
529 def cleanup_apt_cache(self, rootdir):
530 out = self.runcmd(['chroot', rootdir, 'apt-get', 'clean'])
531 logging.debug('stdout:\n%s', out)
532
533 def set_root_password(self, rootdir):
534 if self.settings['root-password']:
535 self.message('Setting root password')
536 self.set_password(rootdir, 'root', self.settings['root-password'])
537 elif self.settings['lock-root-password']:
538 self.message('Locking root password')
539 self.runcmd(['chroot', rootdir, 'passwd', '-l', 'root'])
540 else:
541 self.message('Give root an empty password')
542 self.delete_password(rootdir, 'root')
543
544 def create_users(self, rootdir):
545 def create_user(user):
546 self.runcmd(['chroot', rootdir, 'adduser', '--gecos', user,
547 '--disabled-password', user])
548 if self.settings['sudo']:
549 self.runcmd(['chroot', rootdir, 'adduser', user, 'sudo'])
550
551 for userpass in self.settings['user']:
552 if '/' in userpass:
553 user, password = userpass.split('/', 1)
554 create_user(user)
555 self.set_password(rootdir, user, password)
556 else:
557 create_user(userpass)
558 self.delete_password(rootdir, userpass)
559
560 def set_password(self, rootdir, user, password):
561 encrypted = crypt.crypt(password, '..')
562 self.runcmd(['chroot', rootdir, 'usermod', '-p', encrypted, user])
563
564 def delete_password(self, rootdir, user):
565 self.runcmd(['chroot', rootdir, 'passwd', '-d', user])
566
567 def remove_udev_persistent_rules(self, rootdir):
568 self.message('Removing udev persistent cd and net rules')
569 for x in ['70-persistent-cd.rules', '70-persistent-net.rules']:
570 pathname = os.path.join(rootdir, 'etc', 'udev', 'rules.d', x)
571 if os.path.exists(pathname):
572 logging.debug('rm %s', pathname)
573 os.remove(pathname)
574 else:
575 logging.debug('not removing non-existent %s', pathname)
576
577 def setup_networking(self, rootdir):
578 self.message('Setting up networking')
579
580 f = open(os.path.join(rootdir, 'etc', 'network', 'interfaces'), 'w')
581 f.write('auto lo\n')
582 f.write('iface lo inet loopback\n')
583
584 if self.settings['enable-dhcp']:
585 f.write('\n')
586 f.write('auto eth0\n')
587 f.write('iface eth0 inet dhcp\n')
588
589 f.close()
590
591 def append_serial_console(self, rootdir):
592 if self.settings['serial-console']:
593 serial_command = self.settings['serial-console-command']
594 logging.debug('adding getty to serial console')
595 inittab = os.path.join(rootdir, 'etc/inittab')
596 # to autologin, serial_command can contain '-a root'
597 with open(inittab, 'a') as f:
598 f.write('\nS0:23:respawn:%s\n' % serial_command)
599
600 def install_grub2(self, rootdev, rootdir):
601 self.message("Configuring grub2")
602 # rely on kpartx using consistent naming to map loop0p1 to loop0
603 install_dev = os.path.join('/dev', os.path.basename(rootdev)[:-2])
604 self.runcmd(['mount', '/dev', '-t', 'devfs', '-obind',
605 '%s' % os.path.join(rootdir, 'dev')])
606 self.runcmd(['mount', '/proc', '-t', 'proc', '-obind',
607 '%s' % os.path.join(rootdir, 'proc')])
608 self.runcmd(['mount', '/sys', '-t', 'sysfs', '-obind',
609 '%s' % os.path.join(rootdir, 'sys')])
610
611 cmdline = 'GRUB_CMDLINE_LINUX_DEFAULT="console=tty0 console=tty1 console=ttyS0,38400n8"'
612 terminal = 'GRUB_TERMINAL="serial gfxterm"'
613 command = 'GRUB_SERIAL_COMMAND="serial --speed=38400 --unit=0 --parity=no --stop=1"'
614 grub_cfg = os.path.join(rootdir, 'etc', 'default', 'grub')
615 logging.debug("Allowing serial output in grub config %s", grub_cfg)
616 with open(grub_cfg, 'a+') as cfg:
617 cfg.write("# %s serial support\n" % os.path.basename(__file__))
618 cfg.write("%s\n" % cmdline)
619 cfg.write("%s\n" % terminal)
620 cfg.write("%s\n" % command)
621 try:
622 self.runcmd(['chroot', rootdir, 'update-grub'])
623 self.runcmd(['chroot', rootdir, 'grub-install', install_dev])
624 except cliapp.AppException:
625 self.message("Failed. Is grub2-common installed? Using extlinux.")
626 self.runcmd(['umount', os.path.join(rootdir, 'sys')])
627 self.runcmd(['umount', os.path.join(rootdir, 'proc')])
628 self.runcmd(['umount', os.path.join(rootdir, 'dev')])
629 self.install_extlinux(rootdev, rootdir)
630
631 def install_extlinux(self, rootdev, rootdir):
632 if not os.path.exists("/usr/bin/extlinux"):
633 self.message("extlinux not installed, skipping.")
634 return
635 self.message('Installing extlinux')
636
637 def find(pattern):
638 dirname = os.path.join(rootdir, 'boot')
639 basenames = os.listdir(dirname)
640 logging.debug('find: %s', basenames)
641 for basename in basenames:
642 if re.search(pattern, basename):
643 return os.path.join('boot', basename)
644 raise cliapp.AppException('Cannot find match: %s' % pattern)
645
646 try:
647 kernel_image = find('vmlinuz-.*')
648 initrd_image = find('initrd.img-.*')
649 except cliapp.AppException as e:
650 self.message("Unable to find kernel. Not installing extlinux.")
651 logging.debug("No kernel found. %s. Skipping install of extlinux.", e)
652 return
653
654 out = self.runcmd(['blkid', '-c', '/dev/null', '-o', 'value',
655 '-s', 'UUID', rootdev])
656 uuid = out.splitlines()[0].strip()
657
658 conf = os.path.join(rootdir, 'extlinux.conf')
659 logging.debug('configure extlinux %s', conf)
660 kserial = 'console=ttyS0,115200' if self.settings['serial-console'] else ''
661 extserial = 'serial 0 115200' if self.settings['serial-console'] else ''
662 msg = '''
663 default linux
664 timeout 1
665
666 label linux
667 kernel %(kernel)s
668 append initrd=%(initrd)s root=UUID=%(uuid)s ro %(kserial)s
669 %(extserial)s
670 ''' % {
671 'kernel': kernel_image, # pylint: disable=bad-continuation
672 'initrd': initrd_image, # pylint: disable=bad-continuation
673 'uuid': uuid, # pylint: disable=bad-continuation
674 'kserial': kserial, # pylint: disable=bad-continuation
675 'extserial': extserial, # pylint: disable=bad-continuation
676 } # pylint: disable=bad-continuation
677 logging.debug("extlinux config:\n%s", msg)
678
679 # python multiline string substitution is just ugly.
680 # use an external file or live with the mangling, no point in
681 # mangling the string to remove spaces just to keep it pretty in source.
682 f = open(conf, 'w')
683 f.write(msg)
684
685 self.runcmd(['extlinux', '--install', rootdir])
686 self.runcmd(['sync'])
687 time.sleep(2)
688
689 def optimize_image(self, rootdir):
690 """
691 Filing up the image with zeros will increase its compression rate
692 """
693 if not self.settings['sparse']:
694 zeros = os.path.join(rootdir, 'ZEROS')
695 self.runcmd_unchecked(['dd', 'if=/dev/zero', 'of=' + zeros, 'bs=1M'])
696 self.runcmd(['rm', '-f', zeros])
697
698 def squash(self):
699 """
700 Run squashfs on the image.
701 """
702 if not os.path.exists('/usr/bin/mksquashfs'):
703 logging.warning("Squash selected but mksquashfs not found!")
704 return
705 self.message("Running mksquashfs")
706 suffixed = "%s.squashfs" % self.settings['image']
707 self.runcmd(['mksquashfs', self.settings['image'],
708 suffixed,
709 '-no-progress', '-comp', 'xz'], ignore_fail=False)
710 os.unlink(self.settings['image'])
711 self.settings['image'] = suffixed
712
713 def cleanup_system(self):
714 # Clean up after any errors.
715
716 self.message('Cleaning up')
717
718 # Umount in the reverse mount order
719 if self.settings['image']:
720 for i in range(len(self.mount_points) - 1, -1, -1):
721 mount_point = self.mount_points[i]
722 try:
723 self.runcmd(['umount', mount_point], ignore_fail=False)
724 except cliapp.AppException:
725 logging.debug("umount failed, sleeping and trying again")
726 time.sleep(5)
727 self.runcmd(['umount', mount_point], ignore_fail=False)
728
729 self.runcmd(['kpartx', '-d', self.settings['image']], ignore_fail=True)
730
731 for dirname in self.remove_dirs:
732 shutil.rmtree(dirname)
733
734 def customize(self, rootdir):
735 script = self.settings['customize']
736 if not script:
737 return
738 if not os.path.exists(script):
739 example = os.path.join("/usr/share/vmdebootstrap/examples/", script)
740 if not os.path.exists(example):
741 self.message("Unable to find %s" % script)
742 return
743 script = example
744 self.message('Running customize script %s' % script)
745 logging.info("rootdir=%s image=%s", rootdir, self.settings['image'])
746 with open('/dev/tty', 'w') as tty:
747 try:
748 cliapp.runcmd([script, rootdir, self.settings['image']], stdout=tty, stderr=tty)
749 except IOError as e:
750 subprocess.call([script, rootdir, self.settings['image']])
751
752 def create_tarball(self, rootdir):
753 # Create a tarball of the disk's contents
754 # shell out to runcmd since it more easily handles rootdir
755 self.message('Creating tarball of disk contents')
756 self.runcmd(['tar', '-cf', self.settings['tarball'], '-C', rootdir, '.'])
757
758 def chown(self):
759 # Change image owner after completed build
760 if self.settings['image']:
761 filename = self.settings['image']
762 elif self.settings['tarball']:
763 filename = self.settings['tarball']
764 else:
765 return
766 self.message("Changing owner to %s" % self.settings["owner"])
767 subprocess.call(["chown", self.settings["owner"], filename])
768
769 def list_installed_pkgs(self, rootdir):
770 # output the list of installed packages for sources identification
771 self.message("Creating a list of installed binary package names")
772 out = self.runcmd(['chroot', rootdir,
773 'dpkg-query', '-W', "-f='${Package}.deb\n'"])
774 with open('dpkg.list', 'w') as dpkg:
775 dpkg.write(out)
776
777 def configure_apt(self, rootdir):
778 # use the distribution and mirror to create an apt source
779 self.message("Configuring apt to use distribution and mirror")
780 conf = os.path.join(rootdir, 'etc', 'apt', 'sources.list.d', 'base.list')
781 logging.debug('configure apt %s', conf)
782 mirror = self.settings['mirror']
783 if self.settings['apt-mirror']:
784 mirror = self.settings['apt-mirror']
785 self.message("Setting apt mirror to %s" % mirror)
786 os.unlink(os.path.join(rootdir, 'etc', 'apt', 'sources.list'))
787 f = open(conf, 'w')
788 line = 'deb %s %s main\n' % (mirror, self.settings['distribution'])
789 f.write(line)
790 line = '#deb-src %s %s main\n' % (mirror, self.settings['distribution'])
791 f.write(line)
792 f.close()
793 # ensure the apt sources have valid lists
794 self.runcmd(['chroot', rootdir, 'apt-get', '-qq', 'update'])
795
796 if __name__ == '__main__':
797 VmDebootstrap(version=__version__).run()