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