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