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