]>
git.siccegge.de Git - forks/vmdebootstrap.git/blob - vmdebootstrap
163ee41d59d3147c48265bb500fc6bcfd690c9df
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 if not using the default [deprecated]')
129 self
.settings
.string_list(
131 'pass additional options to debootstrap'),
132 self
.settings
.boolean(
136 self
.settings
.string(
138 "tar up the disk's contents in FILE",
140 self
.settings
.string(
142 'configure apt to use MIRROR',
144 self
.settings
.string(
146 'use MIRROR as package source (%default)',
148 default
='http://http.debian.net/debian/')
149 self
.settings
.string(
151 'architecture to use (%default)',
153 default
=default_arch
)
154 self
.settings
.string(
156 'release to use (%default)',
159 self
.settings
.string_list(
161 'install PACKAGE onto system')
162 self
.settings
.string_list(
164 'install package in DEB file onto system (not from mirror)',
166 self
.settings
.boolean(
168 'do not install a linux package')
169 self
.settings
.string(
171 'install PACKAGE instead of the default kernel package',
173 self
.settings
.boolean(
175 'enable DHCP on eth0')
176 self
.settings
.string(
180 self
.settings
.boolean(
181 ['lock-root-password'],
182 'lock root account so they cannot login?')
183 self
.settings
.string(
185 'run SCRIPT after setting up system',
187 self
.settings
.string(
189 'set name to HOSTNAME (%default)',
192 self
.settings
.string_list(
194 'create USER with PASSWORD',
195 metavar
='USER/PASSWORD')
196 self
.settings
.boolean(
198 'configure image to use a serial console')
199 self
.settings
.string(
200 ['serial-console-command'],
201 'command to manage the serial console, appended to /etc/inittab (%default)',
203 default
='/sbin/getty -L ttyS0 115200 vt100')
204 self
.settings
.boolean(
206 'install sudo, and if user is created, add them to sudo group')
207 self
.settings
.string(
209 'the user who will own the image when the build is complete.')
210 self
.settings
.boolean(
212 'use squashfs on the final image.')
213 self
.settings
.boolean(
215 'Create an apt source based on the distribution and mirror selected.')
216 self
.settings
.boolean(
218 'Run install-mbr (default if extlinux used)')
219 self
.settings
.boolean(
221 'Install and configure grub2 - disables extlinux.')
222 self
.settings
.boolean(
224 'Do not fill the image with zeros to keep a sparse disk image',
226 self
.settings
.boolean(
228 'Create a list of package names included in the image.')
229 self
.settings
.boolean(
231 'do not install the acpid package',
234 def process_args(self
, args
): # pylint: disable=too-many-branches,too-many-statements
235 if not self
.settings
['image'] and not self
.settings
['tarball']:
236 raise cliapp
.AppException(
237 'You must give disk image filename, or tarball filename')
238 if self
.settings
['image'] and not self
.settings
['size']:
239 raise cliapp
.AppException(
240 'If disk image is specified, you must give image size.')
241 if not self
.debian_info
.valid(self
.settings
['distribution']):
242 if not self
.ubuntu_info
.valid(self
.settings
['distribution']):
243 raise cliapp
.AppException(
244 '%s is not a valid Debian or Ubuntu suite or codename.'
245 % self
.settings
['distribution'])
246 if not self
.settings
['use-uefi'] and self
.settings
['esp-size'] != 5242880:
247 raise cliapp
.AppException(
248 'You must specify use-uefi for esp-size to have effect')
249 if self
.settings
['arch'] in self
.efi_arch_table
and\
250 self
.efi_arch_table
[self
.settings
['arch']]['exclusive'] and\
251 not self
.settings
['use-uefi']:
252 raise cliapp
.AppException(
253 'Only UEFI is supported on %s' % self
.settings
['arch'])
254 elif self
.settings
['use-uefi'] and self
.settings
['arch'] not in self
.efi_arch_table
:
255 raise cliapp
.AppException(
256 '%s is not a supported UEFI architecture' % self
.settings
['arch'])
257 if self
.settings
['use-uefi'] and (
258 self
.settings
['bootsize'] or
259 self
.settings
['bootoffset']):
260 raise cliapp
.AppException(
261 'A separate boot partition is not supported with UEFI')
263 if self
.settings
['use-uefi'] and not self
.settings
['grub']:
264 raise cliapp
.AppException(
265 'UEFI without Grub is not supported.')
267 # wheezy (which became oldstable on 04/25/2015) only had amd64 uefi
268 if self
.was_oldstable(datetime
.date(2015, 4, 26)):
269 if self
.settings
['use-uefi'] and self
.settings
['arch'] != 'amd64':
270 raise cliapp
.AppException(
271 'Only amd64 supports UEFI in Wheezy')
273 if os
.geteuid() != 0:
274 sys
.exit("You need to have root privileges to run this script.")
278 roottype
= self
.settings
['roottype']
281 if self
.settings
['image']:
282 self
.create_empty_image()
283 self
.partition_image()
284 if self
.settings
['mbr'] or self
.settings
['extlinux']:
286 (rootdev
, bootdev
, swapdev
) = self
.setup_kpartx()
287 if self
.settings
['swap'] > 0:
288 self
.message("Creating swap space")
289 self
.runcmd(['mkswap', swapdev
])
290 self
.mkfs(rootdev
, fstype
=roottype
)
291 rootdir
= self
.mount(rootdev
)
292 if self
.settings
['use-uefi']:
293 self
.bootdir
= '%s/%s/%s' % (rootdir
, 'boot', 'efi')
294 logging
.debug("bootdir:%s", self
.bootdir
)
295 self
.mkfs(bootdev
, fstype
='vfat')
296 os
.makedirs(self
.bootdir
)
297 self
.mount(bootdev
, self
.bootdir
)
299 if self
.settings
['boottype']:
300 boottype
= self
.settings
['boottype']
303 self
.mkfs(bootdev
, fstype
=boottype
)
304 self
.bootdir
= '%s/%s' % (rootdir
, 'boot/')
305 os
.mkdir(self
.bootdir
)
306 self
.mount(bootdev
, self
.bootdir
)
308 rootdir
= self
.mkdtemp()
309 self
.debootstrap(rootdir
)
310 self
.set_hostname(rootdir
)
311 self
.create_fstab(rootdir
, rootdev
, roottype
, bootdev
, boottype
)
312 self
.install_debs(rootdir
)
313 self
.set_root_password(rootdir
)
314 self
.create_users(rootdir
)
315 self
.remove_udev_persistent_rules(rootdir
)
316 self
.setup_networking(rootdir
)
317 if self
.settings
['configure-apt'] or self
.settings
['apt-mirror']:
318 self
.configure_apt(rootdir
)
319 self
.customize(rootdir
)
320 self
.cleanup_apt_cache(rootdir
)
321 self
.update_initramfs(rootdir
)
323 if self
.settings
['image']:
324 if self
.settings
['use-uefi']:
325 self
.install_grub_uefi(rootdir
)
326 elif self
.settings
['grub']:
327 self
.install_grub2(rootdev
, rootdir
)
328 elif self
.settings
['extlinux']:
329 self
.install_extlinux(rootdev
, rootdir
)
330 self
.append_serial_console(rootdir
)
331 self
.optimize_image(rootdir
)
332 if self
.settings
['squash']:
334 if self
.settings
['pkglist']:
335 self
.list_installed_pkgs(rootdir
)
337 if self
.settings
['foreign']:
338 os
.unlink('%s/usr/bin/%s' %
339 (rootdir
, os
.path
.basename(self
.settings
['foreign'])))
341 if self
.settings
['tarball']:
342 self
.create_tarball(rootdir
)
344 if self
.settings
['owner']:
346 except BaseException
as e
:
347 self
.message('EEEK! Something bad happened...')
349 db_log
= os
.path
.join(rootdir
, 'debootstrap', 'debootstrap.log')
350 if os
.path
.exists(db_log
):
351 shutil
.copy(db_log
, os
.getcwd())
353 self
.cleanup_system()
356 self
.cleanup_system()
358 def message(self
, msg
):
360 if self
.settings
['verbose']:
363 def runcmd(self
, argv
, stdin
='', ignore_fail
=False, env
=None, **kwargs
):
364 logging
.debug('runcmd: %s %s %s', argv
, env
, kwargs
)
365 p
= subprocess
.Popen(argv
, stdin
=subprocess
.PIPE
,
366 stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
,
368 out
, err
= p
.communicate(stdin
)
369 if p
.returncode
!= 0:
370 msg
= 'command failed: %s\n%s\n%s' % (argv
, out
, err
)
373 raise cliapp
.AppException(msg
)
377 dirname
= tempfile
.mkdtemp()
378 self
.remove_dirs
.append(dirname
)
379 logging
.debug('mkdir %s', dirname
)
382 def mount(self
, device
, path
=None):
384 mount_point
= self
.mkdtemp()
387 self
.message('Mounting %s on %s' % (device
, mount_point
))
388 self
.runcmd(['mount', device
, mount_point
])
389 self
.mount_points
.append(mount_point
)
390 logging
.debug('mounted %s on %s', device
, mount_point
)
393 def create_empty_image(self
):
394 self
.message('Creating disk image')
395 self
.runcmd(['qemu-img', 'create', '-f', 'raw',
396 self
.settings
['image'],
397 str(self
.settings
['size'])])
399 def partition_image(self
):
401 Uses fat16 (msdos) partitioning by default, use part-type to change.
402 If bootoffset is specified, the first actual partition
403 starts at that offset to allow customisation scripts to
404 put bootloader images into the space, e.g. u-boot.
406 self
.message('Creating partitions')
407 self
.runcmd(['parted', '-s', self
.settings
['image'],
408 'mklabel', self
.settings
['part-type']])
411 swap
= 256 * 1024 * 1024
412 if self
.settings
['swap'] > 0:
413 if self
.settings
['swap'] > swap
:
414 swap
= self
.settings
['swap']
416 # minimum 256Mb as default qemu ram is 128Mb
417 logging
.debug("Setting minimum 256Mb swap space")
418 extent
= "%s%%" % int(100 * (self
.settings
['size'] - swap
) / self
.settings
['size'])
420 if self
.settings
['use-uefi']:
421 espsize
= self
.settings
['esp-size'] / (1024 * 1024)
422 self
.message("Using ESP size: %smib %s bytes" % (espsize
, self
.settings
['esp-size']))
423 self
.runcmd(['parted', '-s', self
.settings
['image'],
424 'mkpart', 'primary', 'fat32',
426 self
.runcmd(['parted', '-s', self
.settings
['image'],
427 'set', '1', 'boot', 'on'])
428 self
.runcmd(['parted', '-s', self
.settings
['image'],
429 'set', '1', 'esp', 'on'])
431 if self
.settings
['bootoffset'] and self
.settings
['bootoffset'] is not '0':
432 # turn v.small offsets into something at least possible to create.
433 if self
.settings
['bootoffset'] < 1048576:
436 "Setting bootoffset %smib to allow for %s bytes",
437 partoffset
, self
.settings
['bootoffset'])
439 partoffset
= self
.settings
['bootoffset'] / (1024 * 1024)
440 self
.message("Using bootoffset: %smib %s bytes" % (partoffset
, self
.settings
['bootoffset']))
441 if self
.settings
['bootsize'] and self
.settings
['bootsize'] is not '0%':
442 if self
.settings
['grub'] and not partoffset
:
444 bootsize
= self
.settings
['bootsize'] / (1024 * 1024)
445 bootsize
+= partoffset
446 self
.message("Using bootsize %smib: %s bytes" % (bootsize
, self
.settings
['bootsize']))
447 logging
.debug("Starting boot partition at %sMb", bootsize
)
448 self
.runcmd(['parted', '-s', self
.settings
['image'],
449 'mkpart', 'primary', 'fat16', str(partoffset
), str(bootsize
)])
450 logging
.debug("Starting root partition at %sMb", partoffset
)
451 self
.runcmd(['parted', '-s', self
.settings
['image'],
452 'mkpart', 'primary', str(bootsize
), extent
])
453 elif self
.settings
['use-uefi']:
454 bootsize
= self
.settings
['esp-size'] / (1024 * 1024) + 1
455 self
.runcmd(['parted', '-s', self
.settings
['image'],
456 'mkpart', 'primary', str(bootsize
), extent
])
458 self
.runcmd(['parted', '-s', self
.settings
['image'],
459 'mkpart', 'primary', '0%', extent
])
460 self
.runcmd(['parted', '-s', self
.settings
['image'],
461 'set', '1', 'boot', 'on'])
462 if self
.settings
['swap'] > 0:
463 logging
.debug("Creating swap partition")
464 self
.runcmd(['parted', '-s', self
.settings
['image'],
465 'mkpart', 'primary', 'linux-swap', extent
, '100%'])
467 def update_initramfs(self
, rootdir
):
468 cmd
= os
.path
.join('usr', 'sbin', 'update-initramfs')
469 if os
.path
.exists(os
.path
.join(rootdir
, cmd
)):
470 self
.message("Updating the initramfs")
471 self
.runcmd(['chroot', rootdir
, cmd
, '-u'])
473 def install_mbr(self
):
474 if os
.path
.exists("/sbin/install-mbr"):
475 self
.message('Installing MBR')
476 self
.runcmd(['install-mbr', self
.settings
['image']])
478 msg
= "mbr enabled but /sbin/install-mbr not found" \
479 " - please install the mbr package."
480 raise cliapp
.AppException(msg
)
482 def setup_kpartx(self
):
485 out
= self
.runcmd(['kpartx', '-avs', self
.settings
['image']])
486 if self
.settings
['bootsize'] and self
.settings
['swap'] > 0:
491 elif self
.settings
['use-uefi']:
495 elif self
.settings
['use-uefi'] and self
.settings
['swap'] > 0:
500 elif self
.settings
['bootsize']:
504 elif self
.settings
['swap'] > 0:
513 devices
= [line
.split()[2]
514 for line
in out
.splitlines()
515 if line
.startswith('add map ')]
516 if len(devices
) != parts
:
517 msg
= 'Surprising number of partitions - check output of losetup -a'
518 logging
.debug("%s", self
.runcmd(['losetup', '-a']))
519 logging
.debug("%s: devices=%s parts=%s", msg
, devices
, parts
)
520 raise cliapp
.AppException(msg
)
521 root
= '/dev/mapper/%s' % devices
[rootindex
]
522 if self
.settings
['bootsize'] or self
.settings
['use-uefi']:
523 boot
= '/dev/mapper/%s' % devices
[bootindex
]
524 if self
.settings
['swap'] > 0:
525 swap
= '/dev/mapper/%s' % devices
[swapindex
]
526 return root
, boot
, swap
528 def _efi_packages(self
):
530 pkg
= self
.efi_arch_table
[self
.settings
['arch']]['package']
531 self
.message("Adding %s" % pkg
)
533 extra
= self
.efi_arch_table
[self
.settings
['arch']]['extra']
534 if extra
and isinstance(extra
, str):
535 bin_pkg
= self
.efi_arch_table
[str(extra
)]['bin_package']
536 self
.message("Adding support for %s using %s" % (extra
, bin_pkg
))
537 packages
.append(bin_pkg
)
540 def _copy_efi_binary(self
, efi_removable
, efi_install
):
541 logging
.debug("using bootdir=%s", self
.bootdir
)
542 logging
.debug("moving %s to %s", efi_removable
, efi_install
)
543 if efi_removable
.startswith('/'):
544 efi_removable
= efi_removable
[1:]
545 if efi_install
.startswith('/'):
546 efi_install
= efi_install
[1:]
547 efi_output
= os
.path
.join(self
.bootdir
, efi_removable
)
548 efi_input
= os
.path
.join(self
.bootdir
, efi_install
)
549 if not os
.path
.exists(efi_input
):
550 logging
.warning("%s does not exist (%s)", efi_input
, efi_install
)
551 raise cliapp
.AppException("Missing %s" % efi_install
)
552 if not os
.path
.exists(os
.path
.dirname(efi_output
)):
553 os
.makedirs(os
.path
.dirname(efi_output
))
555 'Moving UEFI support: %s -> %s', efi_input
, efi_output
)
556 if os
.path
.exists(efi_output
):
557 os
.unlink(efi_output
)
558 os
.rename(efi_input
, efi_output
)
560 def configure_efi(self
):
562 Copy the bootloader file from the package into the location
563 so needs to be after grub and kernel already installed.
565 self
.message('Configuring EFI')
566 efi_removable
= str(self
.efi_arch_table
[self
.settings
['arch']]['removable'])
567 efi_install
= str(self
.efi_arch_table
[self
.settings
['arch']]['install'])
568 self
.message('Installing UEFI support binary')
569 self
._copy
_efi
_binary
(efi_removable
, efi_install
)
571 def configure_extra_efi(self
):
572 extra
= str(self
.efi_arch_table
[self
.settings
['arch']]['extra'])
574 efi_removable
= str(self
.efi_arch_table
[extra
]['removable'])
575 efi_install
= str(self
.efi_arch_table
[extra
]['install'])
576 self
.message('Copying UEFI support binary for %s' % extra
)
577 self
._copy
_efi
_binary
(efi_removable
, efi_install
)
579 def mkfs(self
, device
, fstype
):
580 self
.message('Creating filesystem %s' % fstype
)
581 self
.runcmd(['mkfs', '-t', fstype
, device
])
583 def suite_to_codename(self
, distro
):
584 suite
= self
.debian_info
.codename(distro
, datetime
.date
.today())
589 def was_oldstable(self
, limit
):
590 suite
= self
.suite_to_codename(self
.settings
['distribution'])
591 # this check is only for debian
592 if not self
.debian_info
.valid(suite
):
594 return suite
== self
.debian_info
.old(limit
)
596 def was_stable(self
, limit
):
597 suite
= self
.suite_to_codename(self
.settings
['distribution'])
598 # this check is only for debian
599 if not self
.debian_info
.valid(suite
):
601 return suite
== self
.debian_info
.stable(limit
)
603 def debootstrap(self
, rootdir
): # pylint: disable=too-many-statements
604 msg
= "(%s)" % self
.settings
['variant'] if self
.settings
['variant'] else ''
606 'Debootstrapping %s [%s] %s' % (
607 self
.settings
['distribution'], self
.settings
['arch'], msg
))
609 include
= self
.settings
['package']
611 if not self
.settings
['foreign'] and not self
.settings
['no-acpid']:
612 include
.append('acpid')
614 if self
.settings
['grub']:
615 if self
.settings
['use-uefi']:
616 include
.extend(self
._efi
_packages
())
618 include
.append('grub-pc')
620 if not self
.settings
['no-kernel']:
621 if self
.settings
['kernel-package']:
622 kernel_image
= self
.settings
['kernel-package']
624 if self
.settings
['arch'] == 'i386':
625 # wheezy (which became oldstable on 04/25/2015) used '486'
626 if self
.was_oldstable(datetime
.date(2015, 4, 26)):
630 elif self
.settings
['arch'] == 'armhf':
631 kernel_arch
= 'armmp'
633 kernel_arch
= self
.settings
['arch']
634 kernel_image
= 'linux-image-%s' % kernel_arch
635 include
.append(kernel_image
)
637 if self
.settings
['sudo'] and 'sudo' not in include
:
638 include
.append('sudo')
640 args
= ['debootstrap', '--arch=%s' % self
.settings
['arch']]
642 if self
.settings
['package']:
644 '--include=%s' % ','.join(include
))
645 if self
.settings
['foreign']:
646 args
.append('--foreign')
647 if self
.settings
['debootstrapopts']:
648 for opt
in self
.settings
['debootstrapopts']:
649 args
.append('--%s' % opt
)
650 elif self
.settings
['variant']:
651 args
.append('--variant')
652 args
.append(self
.settings
['variant'])
653 args
+= [self
.settings
['distribution'],
654 rootdir
, self
.settings
['mirror']]
655 logging
.debug(" ".join(args
))
657 if self
.settings
['foreign']:
658 # set a noninteractive debconf environment for secondstage
660 "DEBIAN_FRONTEND": "noninteractive",
661 "DEBCONF_NONINTERACTIVE_SEEN": "true",
664 # add the mapping to the complete environment.
665 env
.update(os
.environ
)
666 # First copy the binfmt handler over
667 self
.message('Setting up binfmt handler')
668 shutil
.copy(self
.settings
['foreign'], '%s/usr/bin/' % rootdir
)
669 # Next, run the package install scripts etc.
670 self
.message('Running debootstrap second stage')
671 self
.runcmd(['chroot', rootdir
,
672 '/debootstrap/debootstrap', '--second-stage'],
675 def set_hostname(self
, rootdir
):
676 hostname
= self
.settings
['hostname']
677 with
open(os
.path
.join(rootdir
, 'etc', 'hostname'), 'w') as f
:
678 f
.write('%s\n' % hostname
)
680 etc_hosts
= os
.path
.join(rootdir
, 'etc', 'hosts')
682 with
open(etc_hosts
, 'r') as f
:
684 with
open(etc_hosts
, 'w') as f
:
685 for line
in data
.splitlines():
686 if line
.startswith('127.0.0.1'):
687 line
+= ' %s' % hostname
688 f
.write('%s\n' % line
)
692 def create_fstab(self
, rootdir
, rootdev
, roottype
, bootdev
, boottype
): # pylint: disable=too-many-arguments
694 out
= self
.runcmd(['blkid', '-c', '/dev/null', '-o', 'value',
695 '-s', 'UUID', device
])
696 return out
.splitlines()[0].strip()
699 rootdevstr
= 'UUID=%s' % fsuuid(rootdev
)
701 rootdevstr
= '/dev/sda1'
703 if bootdev
and not self
.settings
['use-uefi']:
704 bootdevstr
= 'UUID=%s' % fsuuid(bootdev
)
708 fstab
= os
.path
.join(rootdir
, 'etc', 'fstab')
709 with
open(fstab
, 'w') as f
:
710 f
.write('proc /proc proc defaults 0 0\n')
711 f
.write('%s / %s errors=remount-ro 0 1\n' % (rootdevstr
, roottype
))
713 f
.write('%s /boot %s errors=remount-ro 0 2\n' % (bootdevstr
, boottype
))
714 if self
.settings
['swap'] > 0:
715 f
.write("/dev/sda3 swap swap defaults 0 0\n")
716 elif self
.settings
['swap'] > 0:
717 f
.write("/dev/sda2 swap swap defaults 0 0\n")
719 def install_debs(self
, rootdir
):
720 if not self
.settings
['custom-package']:
722 self
.message('Installing custom packages')
723 tmp
= os
.path
.join(rootdir
, 'tmp', 'install_debs')
725 for deb
in self
.settings
['custom-package']:
726 shutil
.copy(deb
, tmp
)
727 filenames
= [os
.path
.join('/tmp/install_debs', os
.path
.basename(deb
))
728 for deb
in self
.settings
['custom-package']]
730 self
.runcmd_unchecked(['chroot', rootdir
, 'dpkg', '-i'] + filenames
)
731 logging
.debug('stdout:\n%s', out
)
732 logging
.debug('stderr:\n%s', err
)
733 out
= self
.runcmd(['chroot', rootdir
,
734 'apt-get', '-f', '--no-remove', 'install'])
735 logging
.debug('stdout:\n%s', out
)
738 def cleanup_apt_cache(self
, rootdir
):
739 out
= self
.runcmd(['chroot', rootdir
, 'apt-get', 'clean'])
740 logging
.debug('stdout:\n%s', out
)
742 def set_root_password(self
, rootdir
):
743 if self
.settings
['root-password']:
744 self
.message('Setting root password')
745 self
.set_password(rootdir
, 'root', self
.settings
['root-password'])
746 elif self
.settings
['lock-root-password']:
747 self
.message('Locking root password')
748 self
.runcmd(['chroot', rootdir
, 'passwd', '-l', 'root'])
750 self
.message('Give root an empty password')
751 self
.delete_password(rootdir
, 'root')
753 def create_users(self
, rootdir
):
754 def create_user(vmuser
):
755 self
.runcmd(['chroot', rootdir
, 'adduser', '--gecos', vmuser
,
756 '--disabled-password', vmuser
])
757 if self
.settings
['sudo']:
758 self
.runcmd(['chroot', rootdir
, 'adduser', vmuser
, 'sudo'])
760 for userpass
in self
.settings
['user']:
762 user
, password
= userpass
.split('/', 1)
764 self
.set_password(rootdir
, user
, password
)
766 create_user(userpass
)
767 self
.delete_password(rootdir
, userpass
)
769 def set_password(self
, rootdir
, user
, password
):
770 encrypted
= crypt
.crypt(password
, '..')
771 self
.runcmd(['chroot', rootdir
, 'usermod', '-p', encrypted
, user
])
773 def delete_password(self
, rootdir
, user
):
774 self
.runcmd(['chroot', rootdir
, 'passwd', '-d', user
])
776 def remove_udev_persistent_rules(self
, rootdir
):
777 self
.message('Removing udev persistent cd and net rules')
778 for x
in ['70-persistent-cd.rules', '70-persistent-net.rules']:
779 pathname
= os
.path
.join(rootdir
, 'etc', 'udev', 'rules.d', x
)
780 if os
.path
.exists(pathname
):
781 logging
.debug('rm %s', pathname
)
784 logging
.debug('not removing non-existent %s', pathname
)
786 def setup_networking(self
, rootdir
):
787 self
.message('Setting up networking')
789 # unconditionally write for wheezy (which became oldstable on 04/25/2015)
790 if self
.was_oldstable(datetime
.date(2015, 4, 26)):
791 with
open(os
.path
.join(rootdir
, 'etc', 'network', 'interfaces'), 'w') as netfile
:
792 netfile
.write('source /etc/network/interfaces.d/*\n')
793 os
.mkdir(os
.path
.join(rootdir
, 'etc', 'network', 'interfaces.d'))
795 elif not os
.path
.exists(os
.path
.join(rootdir
, 'etc', 'network', 'interfaces')):
796 iface_path
= os
.path
.join(rootdir
, 'etc', 'network', 'interfaces')
797 with
open(iface_path
, 'w') as netfile
:
798 netfile
.write('source-directory /etc/network/interfaces.d\n')
799 ethpath
= os
.path
.join(rootdir
, 'etc', 'network', 'interfaces.d', 'setup')
800 with
open(ethpath
, 'w') as eth
:
801 eth
.write('auto lo\n')
802 eth
.write('iface lo inet loopback\n')
804 if self
.settings
['enable-dhcp']:
806 eth
.write('auto eth0\n')
807 eth
.write('iface eth0 inet dhcp\n')
809 def append_serial_console(self
, rootdir
):
810 if self
.settings
['serial-console']:
811 serial_command
= self
.settings
['serial-console-command']
812 logging
.debug('adding getty to serial console')
813 inittab
= os
.path
.join(rootdir
, 'etc/inittab')
814 # to autologin, serial_command can contain '-a root'
815 with
open(inittab
, 'a') as f
:
816 f
.write('\nS0:23:respawn:%s\n' % serial_command
)
818 # pylint: disable=no-self-use
819 def _grub_serial_console(self
, rootdir
):
820 cmdline
= 'GRUB_CMDLINE_LINUX_DEFAULT="console=tty0 console=tty1 console=ttyS0,38400n8"'
821 terminal
= 'GRUB_TERMINAL="serial gfxterm"'
822 command
= 'GRUB_SERIAL_COMMAND="serial --speed=38400 --unit=0 --parity=no --stop=1"'
823 grub_cfg
= os
.path
.join(rootdir
, 'etc', 'default', 'grub')
824 logging
.debug("Allowing serial output in grub config %s", grub_cfg
)
825 with
open(grub_cfg
, 'a+') as cfg
:
826 cfg
.write("# %s serial support\n" % os
.path
.basename(__file__
))
827 cfg
.write("%s\n" % cmdline
)
828 cfg
.write("%s\n" % terminal
)
829 cfg
.write("%s\n" % command
)
831 def _mount_wrapper(self
, rootdir
):
832 self
.runcmd(['mount', '/dev', '-t', 'devfs', '-obind',
833 '%s' % os
.path
.join(rootdir
, 'dev')])
834 self
.runcmd(['mount', '/proc', '-t', 'proc', '-obind',
835 '%s' % os
.path
.join(rootdir
, 'proc')])
836 self
.runcmd(['mount', '/sys', '-t', 'sysfs', '-obind',
837 '%s' % os
.path
.join(rootdir
, 'sys')])
839 def _umount_wrapper(self
, rootdir
):
840 self
.runcmd(['umount', os
.path
.join(rootdir
, 'sys')])
841 self
.runcmd(['umount', os
.path
.join(rootdir
, 'proc')])
842 self
.runcmd(['umount', os
.path
.join(rootdir
, 'dev')])
844 def install_grub_uefi(self
, rootdir
):
845 self
.message("Configuring grub-uefi")
846 target
= self
.efi_arch_table
[self
.settings
['arch']]['target']
847 grub_opts
= "--target=%s" % target
848 logging
.debug("Running grub-install with options: %s", grub_opts
)
849 self
._mount
_wrapper
(rootdir
)
851 self
.runcmd(['chroot', rootdir
, 'update-grub'])
852 self
.runcmd(['chroot', rootdir
, 'grub-install', grub_opts
])
853 except cliapp
.AppException
as exc
:
856 "Failed to configure grub-uefi for %s" %
857 self
.settings
['arch'])
858 self
._umount
_wrapper
(rootdir
)
860 extra
= str(self
.efi_arch_table
[self
.settings
['arch']]['extra'])
862 target
= self
.efi_arch_table
[extra
]['target']
863 grub_opts
= "--target=%s" % target
865 self
.runcmd(['chroot', rootdir
, 'update-grub'])
866 self
.runcmd(['chroot', rootdir
, 'grub-install', grub_opts
])
867 except cliapp
.AppException
as exc
:
870 "Failed to configure grub-uefi for %s" % extra
)
871 self
.configure_extra_efi()
872 self
._umount
_wrapper
(rootdir
)
874 def install_grub2(self
, rootdev
, rootdir
):
875 self
.message("Configuring grub2")
876 # rely on kpartx using consistent naming to map loop0p1 to loop0
877 grub_opts
= os
.path
.join('/dev', os
.path
.basename(rootdev
)[:-2])
878 if self
.settings
['serial-console']:
879 self
._grub
_serial
_console
(rootdir
)
880 logging
.debug("Running grub-install with options: %s", grub_opts
)
881 self
._mount
_wrapper
(rootdir
)
883 self
.runcmd(['chroot', rootdir
, 'update-grub'])
884 self
.runcmd(['chroot', rootdir
, 'grub-install', grub_opts
])
885 except cliapp
.AppException
as exc
:
887 self
.message("Failed. Is grub2-common installed? Using extlinux.")
888 self
.install_extlinux(rootdev
, rootdir
)
889 self
._umount
_wrapper
(rootdir
)
891 def install_extlinux(self
, rootdev
, rootdir
):
892 if not os
.path
.exists("/usr/bin/extlinux"):
893 self
.message("extlinux not installed, skipping.")
895 self
.message('Installing extlinux')
898 dirname
= os
.path
.join(rootdir
, 'boot')
899 basenames
= os
.listdir(dirname
)
900 logging
.debug('find: %s', basenames
)
901 for basename
in basenames
:
902 if re
.search(pattern
, basename
):
903 return os
.path
.join('boot', basename
)
904 raise cliapp
.AppException('Cannot find match: %s' % pattern
)
907 kernel_image
= find('vmlinuz-.*')
908 initrd_image
= find('initrd.img-.*')
909 except cliapp
.AppException
as e
:
910 self
.message("Unable to find kernel. Not installing extlinux.")
911 logging
.debug("No kernel found. %s. Skipping install of extlinux.", e
)
914 out
= self
.runcmd(['blkid', '-c', '/dev/null', '-o', 'value',
915 '-s', 'UUID', rootdev
])
916 uuid
= out
.splitlines()[0].strip()
918 conf
= os
.path
.join(rootdir
, 'extlinux.conf')
919 logging
.debug('configure extlinux %s', conf
)
920 kserial
= 'console=ttyS0,115200' if self
.settings
['serial-console'] else ''
921 extserial
= 'serial 0 115200' if self
.settings
['serial-console'] else ''
928 append initrd=%(initrd)s root=UUID=%(uuid)s ro %(kserial)s
931 'kernel': kernel_image
, # pylint: disable=bad-continuation
932 'initrd': initrd_image
, # pylint: disable=bad-continuation
933 'uuid': uuid
, # pylint: disable=bad-continuation
934 'kserial': kserial
, # pylint: disable=bad-continuation
935 'extserial': extserial
, # pylint: disable=bad-continuation
936 } # pylint: disable=bad-continuation
937 logging
.debug("extlinux config:\n%s", msg
)
939 # python multiline string substitution is just ugly.
940 # use an external file or live with the mangling, no point in
941 # mangling the string to remove spaces just to keep it pretty in source.
945 self
.runcmd(['extlinux', '--install', rootdir
])
946 self
.runcmd(['sync'])
949 def optimize_image(self
, rootdir
):
951 Filing up the image with zeros will increase its compression rate
953 if not self
.settings
['sparse']:
954 zeros
= os
.path
.join(rootdir
, 'ZEROS')
955 self
.runcmd_unchecked(['dd', 'if=/dev/zero', 'of=' + zeros
, 'bs=1M'])
956 self
.runcmd(['rm', '-f', zeros
])
960 Run squashfs on the image.
962 if not os
.path
.exists('/usr/bin/mksquashfs'):
963 logging
.warning("Squash selected but mksquashfs not found!")
966 "%s usage: %s", self
.settings
['image'],
967 self
.runcmd(['du', self
.settings
['image']]))
968 self
.message("Running mksquashfs")
969 suffixed
= "%s.squashfs" % self
.settings
['image']
970 if os
.path
.exists(suffixed
):
973 ['mksquashfs', self
.settings
['image'],
975 '-no-progress', '-comp', 'xz'], ignore_fail
=False)
977 check_size
= os
.path
.getsize(suffixed
)
978 if check_size
< (1024 * 1024):
980 "%s appears to be too small! %s bytes",
981 suffixed
, check_size
)
983 logging
.debug("squashed size: %s", check_size
)
984 os
.unlink(self
.settings
['image'])
985 self
.settings
['image'] = suffixed
987 "%s usage: %s", self
.settings
['image'],
988 self
.runcmd(['du', self
.settings
['image']]))
990 def cleanup_system(self
):
991 # Clean up after any errors.
993 self
.message('Cleaning up')
995 # Umount in the reverse mount order
996 if self
.settings
['image']:
997 for i
in range(len(self
.mount_points
) - 1, -1, -1):
998 mount_point
= self
.mount_points
[i
]
1000 self
.runcmd(['umount', mount_point
], ignore_fail
=False)
1001 except cliapp
.AppException
:
1002 logging
.debug("umount failed, sleeping and trying again")
1004 self
.runcmd(['umount', mount_point
], ignore_fail
=False)
1006 self
.runcmd(['kpartx', '-d', self
.settings
['image']], ignore_fail
=True)
1008 for dirname
in self
.remove_dirs
:
1009 shutil
.rmtree(dirname
)
1011 def customize(self
, rootdir
):
1012 script
= self
.settings
['customize']
1015 if not os
.path
.exists(script
):
1016 example
= os
.path
.join("/usr/share/vmdebootstrap/examples/", script
)
1017 if not os
.path
.exists(example
):
1018 self
.message("Unable to find %s" % script
)
1021 self
.message('Running customize script %s' % script
)
1022 logging
.info("rootdir=%s image=%s", rootdir
, self
.settings
['image'])
1024 "%s usage: %s", self
.settings
['image'],
1025 self
.runcmd(['du', self
.settings
['image']]))
1026 with
open('/dev/tty', 'w') as tty
:
1028 cliapp
.runcmd([script
, rootdir
, self
.settings
['image']], stdout
=tty
, stderr
=tty
)
1030 subprocess
.call([script
, rootdir
, self
.settings
['image']])
1032 "%s usage: %s", self
.settings
['image'],
1033 self
.runcmd(['du', self
.settings
['image']]))
1035 def create_tarball(self
, rootdir
):
1036 # Create a tarball of the disk's contents
1037 # shell out to runcmd since it more easily handles rootdir
1038 self
.message('Creating tarball of disk contents')
1039 self
.runcmd(['tar', '-cf', self
.settings
['tarball'], '-C', rootdir
, '.'])
1042 # Change image owner after completed build
1043 if self
.settings
['image']:
1044 filename
= self
.settings
['image']
1045 elif self
.settings
['tarball']:
1046 filename
= self
.settings
['tarball']
1049 self
.message("Changing owner to %s" % self
.settings
["owner"])
1050 subprocess
.call(["chown", self
.settings
["owner"], filename
])
1052 def list_installed_pkgs(self
, rootdir
):
1053 # output the list of installed packages for sources identification
1054 self
.message("Creating a list of installed binary package names")
1055 out
= self
.runcmd(['chroot', rootdir
,
1056 'dpkg-query', '-W', "-f='${Package}.deb\n'"])
1057 with
open('dpkg.list', 'w') as dpkg
:
1060 def configure_apt(self
, rootdir
):
1061 # use the distribution and mirror to create an apt source
1062 self
.message("Configuring apt to use distribution and mirror")
1063 conf
= os
.path
.join(rootdir
, 'etc', 'apt', 'sources.list.d', 'base.list')
1064 logging
.debug('configure apt %s', conf
)
1065 mirror
= self
.settings
['mirror']
1066 if self
.settings
['apt-mirror']:
1067 mirror
= self
.settings
['apt-mirror']
1068 self
.message("Setting apt mirror to %s" % mirror
)
1069 os
.unlink(os
.path
.join(rootdir
, 'etc', 'apt', 'sources.list'))
1071 line
= 'deb %s %s main\n' % (mirror
, self
.settings
['distribution'])
1073 line
= '#deb-src %s %s main\n' % (mirror
, self
.settings
['distribution'])
1076 # ensure the apt sources have valid lists
1077 self
.runcmd(['chroot', rootdir
, 'apt-get', '-qq', 'update'])
1079 if __name__
== '__main__':
1080 VmDebootstrap(version
=__version__
).run()