]>
git.siccegge.de Git - forks/vmdebootstrap.git/blob - vmdebootstrap
2 # Copyright 2011-2013 Lars Wirzenius
3 # Copyright 2012 Codethink Limited
4 # Copyright 2014-2015 Neil Williams <codehelp@debian.org>
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.
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.
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/>.
30 from distro_info
import DebianDistroInfo
, UbuntuDistroInfo
35 # pylint: disable=invalid-name,line-too-long,missing-docstring,too-many-branches
38 class VmDebootstrap(cliapp
.Application
): # pylint: disable=too-many-public-methods
40 def __init__(self
, progname
=None, version
=__version__
, description
=None, epilog
=None):
41 super(VmDebootstrap
, self
).__init
__(progname
, version
, description
, epilog
)
43 self
.mount_points
= []
44 self
.debian_info
= DebianDistroInfo()
45 self
.ubuntu_info
= UbuntuDistroInfo()
47 self
.efi_arch_table
= {
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
58 'removable': '/EFI/boot/bootia32.efi',
59 'install': '/EFI/debian/grubia32.efi',
60 'package': 'grub-efi-ia32',
61 'bin_package': 'grub-efi-ia32-bin',
67 'removable': '/EFI/boot/bootaa64.efi',
68 'install': '/EFI/debian/grubaa64.efi',
69 'package': 'grub-efi-arm64',
70 'bin_package': 'grub-efi-arm64-bin',
73 'target': 'arm64-efi',
77 def add_settings(self
):
78 default_arch
= subprocess
.check_output(
79 ["dpkg", "--print-architecture"]).strip()
81 self
.settings
.boolean(
82 ['verbose'], 'report what is going on')
84 ['image'], 'put created disk image in FILE',
86 self
.settings
.bytesize(
88 'create a disk image of size SIZE (%default)',
91 self
.settings
.bytesize(
93 'create boot partition of size SIZE (%default)',
98 'specify file system type for /boot/',
100 self
.settings
.bytesize(
102 'Space to leave at start of the image for bootloader',
104 self
.settings
.boolean(
106 'Setup image for UEFI boot',
108 self
.settings
.bytesize(
110 'Size of EFI System Partition - requires use-uefi',
112 self
.settings
.string(
114 'Partition type to use for this image',
116 self
.settings
.string(
118 'specify file system type for /',
120 self
.settings
.bytesize(
122 'create swap space of size SIZE (min 256Mb)')
123 self
.settings
.string(
125 'set up foreign debootstrap environment using provided program (ie binfmt handler)')
126 self
.settings
.string(
128 'select debootstrap variant it not using the default')
129 self
.settings
.boolean(
133 self
.settings
.string(
135 "tar up the disk's contents in FILE",
137 self
.settings
.string(
139 'configure apt to use MIRROR',
141 self
.settings
.string(
143 'use MIRROR as package source (%default)',
145 default
='http://http.debian.net/debian/')
146 self
.settings
.string(
148 'architecture to use (%default)',
150 default
=default_arch
)
151 self
.settings
.string(
153 'release to use (%default)',
156 self
.settings
.string_list(
158 'install PACKAGE onto system')
159 self
.settings
.string_list(
161 'install package in DEB file onto system (not from mirror)',
163 self
.settings
.boolean(
165 'do not install a linux package')
166 self
.settings
.string(
168 'install PACKAGE instead of the default kernel package',
170 self
.settings
.boolean(
172 'enable DHCP on eth0')
173 self
.settings
.string(
177 self
.settings
.boolean(
178 ['lock-root-password'],
179 'lock root account so they cannot login?')
180 self
.settings
.string(
182 'run SCRIPT after setting up system',
184 self
.settings
.string(
186 'set name to HOSTNAME (%default)',
189 self
.settings
.string_list(
191 'create USER with PASSWORD',
192 metavar
='USER/PASSWORD')
193 self
.settings
.boolean(
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)',
200 default
='/sbin/getty -L ttyS0 115200 vt100')
201 self
.settings
.boolean(
203 'install sudo, and if user is created, add them to sudo group')
204 self
.settings
.string(
206 'the user who will own the image when the build is complete.')
207 self
.settings
.boolean(
209 'use squashfs on the final image.')
210 self
.settings
.boolean(
212 'Create an apt source based on the distribution and mirror selected.')
213 self
.settings
.boolean(
215 'Run install-mbr (default if extlinux used)')
216 self
.settings
.boolean(
218 'Install and configure grub2 - disables extlinux.')
219 self
.settings
.boolean(
221 'Do not fill the image with zeros to keep a sparse disk image',
223 self
.settings
.boolean(
225 'Create a list of package names included in the image.')
226 self
.settings
.boolean(
228 'do not install the acpid package',
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')
259 if self
.settings
['use-uefi'] and not self
.settings
['grub']:
260 raise cliapp
.AppException(
261 'UEFI without Grub is not supported.')
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')
269 if os
.geteuid() != 0:
270 sys
.exit("You need to have root privileges to run this script.")
274 roottype
= self
.settings
['roottype']
277 if self
.settings
['image']:
278 self
.create_empty_image()
279 self
.partition_image()
280 if self
.settings
['mbr'] or self
.settings
['extlinux']:
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
)
295 if self
.settings
['boottype']:
296 boottype
= self
.settings
['boottype']
299 self
.mkfs(bootdev
, fstype
=boottype
)
300 self
.bootdir
= '%s/%s' % (rootdir
, 'boot/')
301 os
.mkdir(self
.bootdir
)
302 self
.mount(bootdev
, self
.bootdir
)
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
)
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']:
330 if self
.settings
['pkglist']:
331 self
.list_installed_pkgs(rootdir
)
333 if self
.settings
['foreign']:
334 os
.unlink('%s/usr/bin/%s' %
335 (rootdir
, os
.path
.basename(self
.settings
['foreign'])))
337 if self
.settings
['tarball']:
338 self
.create_tarball(rootdir
)
340 if self
.settings
['owner']:
342 except BaseException
as e
:
343 self
.message('EEEK! Something bad happened...')
345 db_log
= os
.path
.join(rootdir
, 'debootstrap', 'debootstrap.log')
346 if os
.path
.exists(db_log
):
347 shutil
.copy(db_log
, os
.getcwd())
349 self
.cleanup_system()
352 self
.cleanup_system()
354 def message(self
, msg
):
356 if self
.settings
['verbose']:
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
,
364 out
, err
= p
.communicate(stdin
)
365 if p
.returncode
!= 0:
366 msg
= 'command failed: %s\n%s\n%s' % (argv
, out
, err
)
369 raise cliapp
.AppException(msg
)
373 dirname
= tempfile
.mkdtemp()
374 self
.remove_dirs
.append(dirname
)
375 logging
.debug('mkdir %s', dirname
)
378 def mount(self
, device
, path
=None):
380 mount_point
= self
.mkdtemp()
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
)
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'])])
395 def partition_image(self
):
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.
402 self
.message('Creating partitions')
403 self
.runcmd(['parted', '-s', self
.settings
['image'],
404 'mklabel', self
.settings
['part-type']])
407 swap
= 256 * 1024 * 1024
408 if self
.settings
['swap'] > 0:
409 if self
.settings
['swap'] > swap
:
410 swap
= self
.settings
['swap']
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'])
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',
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'])
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:
432 "Setting bootoffset %smib to allow for %s bytes",
433 partoffset
, self
.settings
['bootoffset'])
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
:
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
])
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%'])
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'])
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']])
474 msg
= "mbr enabled but /sbin/install-mbr not found" \
475 " - please install the mbr package."
476 raise cliapp
.AppException(msg
)
478 def setup_kpartx(self
):
481 out
= self
.runcmd(['kpartx', '-avs', self
.settings
['image']])
482 if self
.settings
['bootsize'] and self
.settings
['swap'] > 0:
487 elif self
.settings
['use-uefi']:
491 elif self
.settings
['use-uefi'] and self
.settings
['swap'] > 0:
496 elif self
.settings
['bootsize']:
500 elif self
.settings
['swap'] > 0:
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
524 def _efi_packages(self
):
526 pkg
= self
.efi_arch_table
[self
.settings
['arch']]['package']
527 self
.message("Adding %s" % 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
)
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
))
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
)
556 def configure_efi(self
):
558 Copy the bootloader file from the package into the location
559 so needs to be after grub and kernel already installed.
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
)
567 def configure_extra_efi(self
):
568 extra
= str(self
.efi_arch_table
[self
.settings
['arch']]['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
)
575 def mkfs(self
, device
, fstype
):
576 self
.message('Creating filesystem %s' % fstype
)
577 self
.runcmd(['mkfs', '-t', fstype
, device
])
579 def suite_to_codename(self
, distro
):
580 suite
= self
.debian_info
.codename(distro
, datetime
.date
.today())
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
):
590 return suite
== self
.debian_info
.old(limit
)
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
):
597 return suite
== self
.debian_info
.stable(limit
)
599 def debootstrap(self
, rootdir
): # pylint: disable=too-many-statements
600 msg
= "(%s)" % self
.settings
['variant'] if self
.settings
['variant'] else ''
602 'Debootstrapping %s [%s] %s' % (
603 self
.settings
['distribution'], self
.settings
['arch'], msg
))
605 include
= self
.settings
['package']
607 if not self
.settings
['foreign'] and not self
.settings
['no-acpid']:
608 include
.append('acpid')
610 if self
.settings
['grub']:
611 if self
.settings
['use-uefi']:
612 include
.extend(self
._efi
_packages
())
614 include
.append('grub-pc')
616 if not self
.settings
['no-kernel']:
617 if self
.settings
['kernel-package']:
618 kernel_image
= self
.settings
['kernel-package']
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)):
626 elif self
.settings
['arch'] == 'armhf':
627 kernel_arch
= 'armmp'
629 kernel_arch
= self
.settings
['arch']
630 kernel_image
= 'linux-image-%s' % kernel_arch
631 include
.append(kernel_image
)
633 if self
.settings
['sudo'] and 'sudo' not in include
:
634 include
.append('sudo')
636 args
= ['debootstrap', '--arch=%s' % self
.settings
['arch']]
638 if self
.settings
['package']:
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
))
650 if self
.settings
['foreign']:
651 # set a noninteractive debconf environment for secondstage
653 "DEBIAN_FRONTEND": "noninteractive",
654 "DEBCONF_NONINTERACTIVE_SEEN": "true",
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'],
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
)
673 etc_hosts
= os
.path
.join(rootdir
, 'etc', 'hosts')
675 with
open(etc_hosts
, 'r') as f
:
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
)
685 def create_fstab(self
, rootdir
, rootdev
, roottype
, bootdev
, boottype
): # pylint: disable=too-many-arguments
687 out
= self
.runcmd(['blkid', '-c', '/dev/null', '-o', 'value',
688 '-s', 'UUID', device
])
689 return out
.splitlines()[0].strip()
692 rootdevstr
= 'UUID=%s' % fsuuid(rootdev
)
694 rootdevstr
= '/dev/sda1'
696 if bootdev
and not self
.settings
['use-uefi']:
697 bootdevstr
= 'UUID=%s' % fsuuid(bootdev
)
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
))
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")
712 def install_debs(self
, rootdir
):
713 if not self
.settings
['custom-package']:
715 self
.message('Installing custom packages')
716 tmp
= os
.path
.join(rootdir
, 'tmp', 'install_debs')
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']]
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
)
731 def cleanup_apt_cache(self
, rootdir
):
732 out
= self
.runcmd(['chroot', rootdir
, 'apt-get', 'clean'])
733 logging
.debug('stdout:\n%s', out
)
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'])
743 self
.message('Give root an empty password')
744 self
.delete_password(rootdir
, 'root')
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'])
753 for userpass
in self
.settings
['user']:
755 user
, password
= userpass
.split('/', 1)
757 self
.set_password(rootdir
, user
, password
)
759 create_user(userpass
)
760 self
.delete_password(rootdir
, userpass
)
762 def set_password(self
, rootdir
, user
, password
):
763 encrypted
= crypt
.crypt(password
, '..')
764 self
.runcmd(['chroot', rootdir
, 'usermod', '-p', encrypted
, user
])
766 def delete_password(self
, rootdir
, user
):
767 self
.runcmd(['chroot', rootdir
, 'passwd', '-d', user
])
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
)
777 logging
.debug('not removing non-existent %s', pathname
)
779 def setup_networking(self
, rootdir
):
780 self
.message('Setting up networking')
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'))
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')
797 if self
.settings
['enable-dhcp']:
799 eth
.write('auto eth0\n')
800 eth
.write('iface eth0 inet dhcp\n')
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
)
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
)
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')])
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')])
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
)
844 self
.runcmd(['chroot', rootdir
, 'update-grub'])
845 self
.runcmd(['chroot', rootdir
, 'grub-install', grub_opts
])
846 except cliapp
.AppException
as exc
:
849 "Failed to configure grub-uefi for %s" %
850 self
.settings
['arch'])
851 self
._umount
_wrapper
(rootdir
)
853 extra
= str(self
.efi_arch_table
[self
.settings
['arch']]['extra'])
855 target
= self
.efi_arch_table
[extra
]['target']
856 grub_opts
= "--target=%s" % target
858 self
.runcmd(['chroot', rootdir
, 'update-grub'])
859 self
.runcmd(['chroot', rootdir
, 'grub-install', grub_opts
])
860 except cliapp
.AppException
as exc
:
863 "Failed to configure grub-uefi for %s" % extra
)
864 self
.configure_extra_efi()
865 self
._umount
_wrapper
(rootdir
)
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
)
876 self
.runcmd(['chroot', rootdir
, 'update-grub'])
877 self
.runcmd(['chroot', rootdir
, 'grub-install', grub_opts
])
878 except cliapp
.AppException
as exc
:
880 self
.message("Failed. Is grub2-common installed? Using extlinux.")
881 self
.install_extlinux(rootdev
, rootdir
)
882 self
._umount
_wrapper
(rootdir
)
884 def install_extlinux(self
, rootdev
, rootdir
):
885 if not os
.path
.exists("/usr/bin/extlinux"):
886 self
.message("extlinux not installed, skipping.")
888 self
.message('Installing extlinux')
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
)
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
)
907 out
= self
.runcmd(['blkid', '-c', '/dev/null', '-o', 'value',
908 '-s', 'UUID', rootdev
])
909 uuid
= out
.splitlines()[0].strip()
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 ''
921 append initrd=%(initrd)s root=UUID=%(uuid)s ro %(kserial)s
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
)
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.
938 self
.runcmd(['extlinux', '--install', rootdir
])
939 self
.runcmd(['sync'])
942 def optimize_image(self
, rootdir
):
944 Filing up the image with zeros will increase its compression rate
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
])
953 Run squashfs on the image.
955 if not os
.path
.exists('/usr/bin/mksquashfs'):
956 logging
.warning("Squash selected but mksquashfs not found!")
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
):
966 ['mksquashfs', self
.settings
['image'],
968 '-no-progress', '-comp', 'xz'], ignore_fail
=False)
970 check_size
= os
.path
.getsize(suffixed
)
971 if check_size
< (1024 * 1024):
973 "%s appears to be too small! %s bytes",
974 suffixed
, check_size
)
976 logging
.debug("squashed size: %s", check_size
)
977 os
.unlink(self
.settings
['image'])
978 self
.settings
['image'] = suffixed
980 "%s usage: %s", self
.settings
['image'],
981 self
.runcmd(['du', self
.settings
['image']]))
983 def cleanup_system(self
):
984 # Clean up after any errors.
986 self
.message('Cleaning up')
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
]
993 self
.runcmd(['umount', mount_point
], ignore_fail
=False)
994 except cliapp
.AppException
:
995 logging
.debug("umount failed, sleeping and trying again")
997 self
.runcmd(['umount', mount_point
], ignore_fail
=False)
999 self
.runcmd(['kpartx', '-d', self
.settings
['image']], ignore_fail
=True)
1001 for dirname
in self
.remove_dirs
:
1002 shutil
.rmtree(dirname
)
1004 def customize(self
, rootdir
):
1005 script
= self
.settings
['customize']
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
)
1014 self
.message('Running customize script %s' % script
)
1015 logging
.info("rootdir=%s image=%s", rootdir
, self
.settings
['image'])
1017 "%s usage: %s", self
.settings
['image'],
1018 self
.runcmd(['du', self
.settings
['image']]))
1019 with
open('/dev/tty', 'w') as tty
:
1021 cliapp
.runcmd([script
, rootdir
, self
.settings
['image']], stdout
=tty
, stderr
=tty
)
1023 subprocess
.call([script
, rootdir
, self
.settings
['image']])
1025 "%s usage: %s", self
.settings
['image'],
1026 self
.runcmd(['du', self
.settings
['image']]))
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
, '.'])
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']
1042 self
.message("Changing owner to %s" % self
.settings
["owner"])
1043 subprocess
.call(["chown", self
.settings
["owner"], filename
])
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
:
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'))
1064 line
= 'deb %s %s main\n' % (mirror
, self
.settings
['distribution'])
1066 line
= '#deb-src %s %s main\n' % (mirror
, self
.settings
['distribution'])
1069 # ensure the apt sources have valid lists
1070 self
.runcmd(['chroot', rootdir
, 'apt-get', '-qq', 'update'])
1072 if __name__
== '__main__':
1073 VmDebootstrap(version
=__version__
).run()