]>
git.siccegge.de Git - forks/vmdebootstrap.git/blob - vmdebootstrap
2e7f9e93264fff3930adf63c8b0eb33eaf60a601
2 # Copyright 2011-2013 Lars Wirzenius
3 # Copyright 2012 Codethink Limited
4 # Copyright 2014 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/>.
29 from distro_info
import DebianDistroInfo
, UbuntuDistroInfo
34 # pylint: disable=invalid-name,line-too-long,missing-docstring,too-many-branches
37 class VmDebootstrap(cliapp
.Application
): # pylint: disable=too-many-public-methods
39 def __init__(self
, progname
=None, version
=__version__
, description
=None, epilog
=None):
40 super(VmDebootstrap
, self
).__init
__(progname
, version
, description
, epilog
)
42 self
.mount_points
= []
43 self
.debian_info
= DebianDistroInfo()
44 self
.ubuntu_info
= UbuntuDistroInfo()
46 def add_settings(self
):
47 default_arch
= subprocess
.check_output(
48 ["dpkg", "--print-architecture"]).strip()
50 self
.settings
.boolean(
51 ['verbose'], 'report what is going on')
53 ['image'], 'put created disk image in FILE',
55 self
.settings
.bytesize(
57 'create a disk image of size SIZE (%default)',
60 self
.settings
.bytesize(
62 'create boot partition of size SIZE (%default)',
67 'specify file system type for /boot/',
69 self
.settings
.bytesize(
71 'Space to leave at start of the image for bootloader',
75 'Partition type to use for this image',
79 'specify file system type for /',
81 self
.settings
.bytesize(
83 'create swap space of size SIZE (min 256Mb)')
86 'set up foreign debootstrap environment using provided program (ie binfmt handler)')
89 'select debootstrap variant it not using the default')
90 self
.settings
.boolean(
96 "tar up the disk's contents in FILE",
100 'configure apt to use MIRROR',
102 self
.settings
.string(
104 'use MIRROR as package source (%default)',
106 default
='http://http.debian.net/debian/')
107 self
.settings
.string(
109 'architecture to use (%default)',
111 default
=default_arch
)
112 self
.settings
.string(
114 'release to use (%default)',
117 self
.settings
.string_list(
119 'install PACKAGE onto system')
120 self
.settings
.string_list(
122 'install package in DEB file onto system (not from mirror)',
124 self
.settings
.boolean(
126 'do not install a linux package')
127 self
.settings
.string(
129 'install PACKAGE instead of the default kernel package',
131 self
.settings
.boolean(
133 'enable DHCP on eth0')
134 self
.settings
.string(
138 self
.settings
.boolean(
139 ['lock-root-password'],
140 'lock root account so they cannot login?')
141 self
.settings
.string(
143 'run SCRIPT after setting up system',
145 self
.settings
.string(
147 'set name to HOSTNAME (%default)',
150 self
.settings
.string_list(
152 'create USER with PASSWORD',
153 metavar
='USER/PASSWORD')
154 self
.settings
.boolean(
156 'configure image to use a serial console')
157 self
.settings
.string(
158 ['serial-console-command'],
159 'command to manage the serial console, appended to /etc/inittab (%default)',
161 default
='/sbin/getty -L ttyS0 115200 vt100')
162 self
.settings
.boolean(
164 'install sudo, and if user is created, add them to sudo group')
165 self
.settings
.string(
167 'the user who will own the image when the build is complete.')
168 self
.settings
.boolean(
170 'use squashfs on the final image.')
171 self
.settings
.boolean(
173 'Create an apt source based on the distribution and mirror selected.')
174 self
.settings
.boolean(
176 'Run install-mbr (default if extlinux used)')
177 self
.settings
.boolean(
179 'Install and configure grub2 - disables extlinux.')
180 self
.settings
.boolean(
182 'Do not fill the image with zeros to keep a sparse disk image',
184 self
.settings
.boolean(
186 'Create a list of package names included in the image.')
187 self
.settings
.boolean(
189 'do not install the acpid package',
192 def process_args(self
, args
): # pylint: disable=too-many-branches,too-many-statements
193 if not self
.settings
['image'] and not self
.settings
['tarball']:
194 raise cliapp
.AppException(
195 'You must give disk image filename, or tarball filename')
196 if self
.settings
['image'] and not self
.settings
['size']:
197 raise cliapp
.AppException(
198 'If disk image is specified, you must give image size.')
199 if not self
.debian_info
.valid(self
.settings
['distribution']):
200 if not self
.ubuntu_info
.valid(self
.settings
['distribution']):
201 raise cliapp
.AppException(
202 '%s is not a valid Debian or Ubuntu suite or codename.'
203 % self
.settings
['distribution'])
207 roottype
= self
.settings
['roottype']
210 if self
.settings
['image']:
211 self
.create_empty_image()
212 self
.partition_image()
213 if self
.settings
['mbr'] or self
.settings
['extlinux']:
215 (rootdev
, bootdev
, swapdev
) = self
.setup_kpartx()
216 if self
.settings
['swap'] > 0:
217 self
.message("Creating swap space")
218 self
.runcmd(['mkswap', swapdev
])
219 self
.mkfs(rootdev
, fstype
=roottype
)
220 rootdir
= self
.mount(rootdev
)
222 if self
.settings
['boottype']:
223 boottype
= self
.settings
['boottype']
226 self
.mkfs(bootdev
, fstype
=boottype
)
227 bootdir
= '%s/%s' % (rootdir
, 'boot/')
229 self
.mount(bootdev
, bootdir
)
231 rootdir
= self
.mkdtemp()
232 self
.debootstrap(rootdir
)
233 self
.set_hostname(rootdir
)
234 self
.create_fstab(rootdir
, rootdev
, roottype
, bootdev
, boottype
)
235 self
.install_debs(rootdir
)
236 self
.cleanup_apt_cache(rootdir
)
237 self
.set_root_password(rootdir
)
238 self
.create_users(rootdir
)
239 self
.remove_udev_persistent_rules(rootdir
)
240 self
.setup_networking(rootdir
)
241 if self
.settings
['configure-apt'] or self
.settings
['apt-mirror']:
242 self
.configure_apt(rootdir
)
243 self
.customize(rootdir
)
244 self
.update_initramfs(rootdir
)
246 if self
.settings
['image']:
247 if self
.settings
['grub']:
248 self
.install_grub2(rootdev
, rootdir
)
249 elif self
.settings
['extlinux']:
250 self
.install_extlinux(rootdev
, rootdir
)
251 self
.append_serial_console(rootdir
)
252 self
.optimize_image(rootdir
)
253 if self
.settings
['squash']:
255 if self
.settings
['pkglist']:
256 self
.list_installed_pkgs(rootdir
)
258 if self
.settings
['foreign']:
259 os
.unlink('%s/usr/bin/%s' %
260 (rootdir
, os
.path
.basename(self
.settings
['foreign'])))
262 if self
.settings
['tarball']:
263 self
.create_tarball(rootdir
)
265 if self
.settings
['owner']:
267 except BaseException
as e
:
268 self
.message('EEEK! Something bad happened...')
270 db_log
= os
.path
.join(rootdir
, 'debootstrap', 'debootstrap.log')
271 if os
.path
.exists(db_log
):
272 shutil
.copy(db_log
, os
.getcwd())
274 self
.cleanup_system()
277 self
.cleanup_system()
279 def message(self
, msg
):
281 if self
.settings
['verbose']:
284 def runcmd(self
, argv
, stdin
='', ignore_fail
=False, env
=None, **kwargs
):
285 logging
.debug('runcmd: %s %s %s', argv
, env
, kwargs
)
286 p
= subprocess
.Popen(argv
, stdin
=subprocess
.PIPE
,
287 stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
,
289 out
, err
= p
.communicate(stdin
)
290 if p
.returncode
!= 0:
291 msg
= 'command failed: %s\n%s\n%s' % (argv
, out
, err
)
294 raise cliapp
.AppException(msg
)
298 dirname
= tempfile
.mkdtemp()
299 self
.remove_dirs
.append(dirname
)
300 logging
.debug('mkdir %s', dirname
)
303 def mount(self
, device
, path
=None):
305 mount_point
= self
.mkdtemp()
308 self
.message('Mounting %s on %s' % (device
, mount_point
))
309 self
.runcmd(['mount', device
, mount_point
])
310 self
.mount_points
.append(mount_point
)
311 logging
.debug('mounted %s on %s', device
, mount_point
)
314 def create_empty_image(self
):
315 self
.message('Creating disk image')
316 self
.runcmd(['qemu-img', 'create', '-f', 'raw',
317 self
.settings
['image'],
318 str(self
.settings
['size'])])
320 def partition_image(self
):
322 Uses fat16 (msdos) partitioning by default, use part-type to change.
323 If bootoffset is specified, the first actual partition
324 starts at that offset to allow customisation scripts to
325 put bootloader images into the space, e.g. u-boot.
327 self
.message('Creating partitions')
328 self
.runcmd(['parted', '-s', self
.settings
['image'],
329 'mklabel', self
.settings
['part-type']])
333 swap
= 256 * 1024 * 1024
334 if self
.settings
['swap'] > 0:
335 if self
.settings
['swap'] > swap
:
336 swap
= self
.settings
['swap']
338 # minimum 256Mb as default qemu ram is 128Mb
339 logging
.debug("Setting minimum 256Mb swap space")
340 extent
= "%s%%" % int(100 * (self
.settings
['size'] - swap
) / self
.settings
['size'])
341 if self
.settings
['bootoffset'] and self
.settings
['bootoffset'] is not '0':
342 # turn v.small offsets into something at least possible to create.
343 if self
.settings
['bootoffset'] < 1048576:
346 "Setting bootoffset %smib to allow for %s bytes",
347 partoffset
, self
.settings
['bootoffset'])
349 partoffset
= self
.settings
['bootoffset'] / (1024 * 1024)
350 self
.message("Using bootoffset: %smib %s bytes" % (partoffset
, self
.settings
['bootoffset']))
351 if self
.settings
['bootsize'] and self
.settings
['bootsize'] is not '0%':
352 if self
.settings
['grub'] and not partoffset
:
354 bootsize
= self
.settings
['bootsize'] / (1024 * 1024)
355 bootsize
+= partoffset
356 self
.message("Using bootsize %smib: %s bytes" % (bootsize
, self
.settings
['bootsize']))
357 logging
.debug("Starting boot partition at %sMb", bootsize
)
358 self
.runcmd(['parted', '-s', self
.settings
['image'],
359 'mkpart', 'primary', 'fat16', str(partoffset
), str(bootsize
)])
360 logging
.debug("Starting root partition at %sMb", partoffset
)
361 self
.runcmd(['parted', '-s', self
.settings
['image'],
362 'mkpart', 'primary', str(bootsize
), extent
])
364 self
.runcmd(['parted', '-s', self
.settings
['image'],
365 'mkpart', 'primary', '0%', extent
])
366 self
.runcmd(['parted', '-s', self
.settings
['image'],
367 'set', '1', 'boot', 'on'])
368 if self
.settings
['swap'] > 0:
369 logging
.debug("Creating swap partition")
370 self
.runcmd(['parted', '-s', self
.settings
['image'],
371 'mkpart', 'primary', 'linux-swap', extent
, '100%'])
373 def update_initramfs(self
, rootdir
):
374 cmd
= os
.path
.join('usr', 'sbin', 'update-initramfs')
375 if os
.path
.exists(os
.path
.join(rootdir
, cmd
)):
376 self
.message("Updating the initramfs")
377 self
.runcmd(['chroot', rootdir
, cmd
, '-u'])
379 def install_mbr(self
):
380 if os
.path
.exists("/sbin/install-mbr"):
381 self
.message('Installing MBR')
382 self
.runcmd(['install-mbr', self
.settings
['image']])
384 msg
= "mbr enabled but /sbin/install-mbr not found" \
385 " - please install the mbr package."
386 raise cliapp
.AppException(msg
)
388 def setup_kpartx(self
):
391 out
= self
.runcmd(['kpartx', '-avs', self
.settings
['image']])
392 if self
.settings
['bootsize'] and self
.settings
['swap'] > 0:
397 elif self
.settings
['bootsize']:
401 elif self
.settings
['swap'] > 0:
410 devices
= [line
.split()[2]
411 for line
in out
.splitlines()
412 if line
.startswith('add map ')]
413 if len(devices
) != parts
:
414 msg
= 'Surprising number of partitions - check output of losetup -a'
415 logging
.debug("%s", self
.runcmd(['losetup', '-a']))
416 logging
.debug("%s: devices=%s parts=%s", msg
, devices
, parts
)
417 raise cliapp
.AppException(msg
)
418 root
= '/dev/mapper/%s' % devices
[rootindex
]
419 if self
.settings
['bootsize']:
420 boot
= '/dev/mapper/%s' % devices
[bootindex
]
421 if self
.settings
['swap'] > 0:
422 swap
= '/dev/mapper/%s' % devices
[swapindex
]
423 return root
, boot
, swap
425 def mkfs(self
, device
, fstype
):
426 self
.message('Creating filesystem %s' % fstype
)
427 self
.runcmd(['mkfs', '-t', fstype
, device
])
429 def suite_to_codename(self
, distro
):
430 suite
= self
.debian_info
.codename(distro
, datetime
.date
.today())
435 def was_oldstable(self
, limit
):
436 suite
= self
.suite_to_codename(self
.settings
['distribution'])
437 # this check is only for debian
438 if not self
.debian_info
.valid(suite
):
440 return suite
== self
.debian_info
.old(limit
)
442 def was_stable(self
, limit
):
443 suite
= self
.suite_to_codename(self
.settings
['distribution'])
444 # this check is only for debian
445 if not self
.debian_info
.valid(suite
):
447 return suite
== self
.debian_info
.stable(limit
)
449 def debootstrap(self
, rootdir
):
450 msg
= "(%s)" % self
.settings
['variant'] if self
.settings
['variant'] else ''
451 self
.message('Debootstrapping %s %s' % (self
.settings
['distribution'], msg
))
453 include
= self
.settings
['package']
455 if not self
.settings
['foreign'] and not self
.settings
['no-acpid']:
456 include
.append('acpid')
458 if self
.settings
['grub']:
459 include
.append('grub-pc')
461 if not self
.settings
['no-kernel']:
462 if self
.settings
['kernel-package']:
463 kernel_image
= self
.settings
['kernel-package']
465 if self
.settings
['arch'] == 'i386':
466 # wheezy (which became oldstable on 04/25/2015) used '486'
467 if self
.was_oldstable(datetime
.date(2015, 4, 26)):
471 elif self
.settings
['arch'] == 'armhf':
472 kernel_arch
= 'armmp'
474 kernel_arch
= self
.settings
['arch']
475 kernel_image
= 'linux-image-%s' % kernel_arch
476 include
.append(kernel_image
)
478 if self
.settings
['sudo'] and 'sudo' not in include
:
479 include
.append('sudo')
481 args
= ['debootstrap', '--arch=%s' % self
.settings
['arch']]
483 if self
.settings
['package']:
485 '--include=%s' % ','.join(include
))
486 if self
.settings
['foreign']:
487 args
.append('--foreign')
488 if self
.settings
['variant']:
489 args
.append('--variant')
490 args
.append(self
.settings
['variant'])
491 args
+= [self
.settings
['distribution'],
492 rootdir
, self
.settings
['mirror']]
493 logging
.debug(" ".join(args
))
495 if self
.settings
['foreign']:
496 # set a noninteractive debconf environment for secondstage
498 "DEBIAN_FRONTEND": "noninteractive",
499 "DEBCONF_NONINTERACTIVE_SEEN": "true",
502 # add the mapping to the complete environment.
503 env
.update(os
.environ
)
504 # First copy the binfmt handler over
505 self
.message('Setting up binfmt handler')
506 shutil
.copy(self
.settings
['foreign'], '%s/usr/bin/' % rootdir
)
507 # Next, run the package install scripts etc.
508 self
.message('Running debootstrap second stage')
509 self
.runcmd(['chroot', rootdir
,
510 '/debootstrap/debootstrap', '--second-stage'],
513 def set_hostname(self
, rootdir
):
514 hostname
= self
.settings
['hostname']
515 with
open(os
.path
.join(rootdir
, 'etc', 'hostname'), 'w') as f
:
516 f
.write('%s\n' % hostname
)
518 etc_hosts
= os
.path
.join(rootdir
, 'etc', 'hosts')
520 with
open(etc_hosts
, 'r') as f
:
522 with
open(etc_hosts
, 'w') as f
:
523 for line
in data
.splitlines():
524 if line
.startswith('127.0.0.1'):
525 line
+= ' %s' % hostname
526 f
.write('%s\n' % line
)
530 def create_fstab(self
, rootdir
, rootdev
, roottype
, bootdev
, boottype
): # pylint: disable=too-many-arguments
532 out
= self
.runcmd(['blkid', '-c', '/dev/null', '-o', 'value',
533 '-s', 'UUID', device
])
534 return out
.splitlines()[0].strip()
537 rootdevstr
= 'UUID=%s' % fsuuid(rootdev
)
539 rootdevstr
= '/dev/sda1'
542 bootdevstr
= 'UUID=%s' % fsuuid(bootdev
)
546 fstab
= os
.path
.join(rootdir
, 'etc', 'fstab')
547 with
open(fstab
, 'w') as f
:
548 f
.write('proc /proc proc defaults 0 0\n')
549 f
.write('%s / %s errors=remount-ro 0 1\n' % (rootdevstr
, roottype
))
551 f
.write('%s /boot %s errors=remount-ro 0 2\n' % (bootdevstr
, boottype
))
552 if self
.settings
['swap'] > 0:
553 f
.write("/dev/sda3 swap swap defaults 0 0\n")
554 elif self
.settings
['swap'] > 0:
555 f
.write("/dev/sda2 swap swap defaults 0 0\n")
557 def install_debs(self
, rootdir
):
558 if not self
.settings
['custom-package']:
560 self
.message('Installing custom packages')
561 tmp
= os
.path
.join(rootdir
, 'tmp', 'install_debs')
563 for deb
in self
.settings
['custom-package']:
564 shutil
.copy(deb
, tmp
)
565 filenames
= [os
.path
.join('/tmp/install_debs', os
.path
.basename(deb
))
566 for deb
in self
.settings
['custom-package']]
568 self
.runcmd_unchecked(['chroot', rootdir
, 'dpkg', '-i'] + filenames
)
569 logging
.debug('stdout:\n%s', out
)
570 logging
.debug('stderr:\n%s', err
)
571 out
= self
.runcmd(['chroot', rootdir
,
572 'apt-get', '-f', '--no-remove', 'install'])
573 logging
.debug('stdout:\n%s', out
)
576 def cleanup_apt_cache(self
, rootdir
):
577 out
= self
.runcmd(['chroot', rootdir
, 'apt-get', 'clean'])
578 logging
.debug('stdout:\n%s', out
)
580 def set_root_password(self
, rootdir
):
581 if self
.settings
['root-password']:
582 self
.message('Setting root password')
583 self
.set_password(rootdir
, 'root', self
.settings
['root-password'])
584 elif self
.settings
['lock-root-password']:
585 self
.message('Locking root password')
586 self
.runcmd(['chroot', rootdir
, 'passwd', '-l', 'root'])
588 self
.message('Give root an empty password')
589 self
.delete_password(rootdir
, 'root')
591 def create_users(self
, rootdir
):
592 def create_user(user
):
593 self
.runcmd(['chroot', rootdir
, 'adduser', '--gecos', user
,
594 '--disabled-password', user
])
595 if self
.settings
['sudo']:
596 self
.runcmd(['chroot', rootdir
, 'adduser', user
, 'sudo'])
598 for userpass
in self
.settings
['user']:
600 user
, password
= userpass
.split('/', 1)
602 self
.set_password(rootdir
, user
, password
)
604 create_user(userpass
)
605 self
.delete_password(rootdir
, userpass
)
607 def set_password(self
, rootdir
, user
, password
):
608 encrypted
= crypt
.crypt(password
, '..')
609 self
.runcmd(['chroot', rootdir
, 'usermod', '-p', encrypted
, user
])
611 def delete_password(self
, rootdir
, user
):
612 self
.runcmd(['chroot', rootdir
, 'passwd', '-d', user
])
614 def remove_udev_persistent_rules(self
, rootdir
):
615 self
.message('Removing udev persistent cd and net rules')
616 for x
in ['70-persistent-cd.rules', '70-persistent-net.rules']:
617 pathname
= os
.path
.join(rootdir
, 'etc', 'udev', 'rules.d', x
)
618 if os
.path
.exists(pathname
):
619 logging
.debug('rm %s', pathname
)
622 logging
.debug('not removing non-existent %s', pathname
)
624 def setup_networking(self
, rootdir
):
625 self
.message('Setting up networking')
627 # unconditionally write for wheezy (which became oldstable on 04/25/2015)
628 if self
.was_oldstable(datetime
.date(2015, 4, 26)):
629 with
open(os
.path
.join(
630 rootdir
, 'etc', 'network', 'interfaces'), 'w') as netfile
:
631 netfile
.write('source /etc/network/interfaces.d/*\n')
632 os
.mkdir(os
.path
.join(rootdir
, 'etc', 'network', 'interfaces.d'))
634 elif not os
.path
.exists(os
.path
.join(rootdir
, 'etc', 'network', 'interfaces')):
635 with
open(os
.path
.join(
636 rootdir
, 'etc', 'network', 'interfaces'), 'w') as netfile
:
637 netfile
.write('source-directory /etc/network/interfaces.d\n')
639 with
open(os
.path
.join(
640 rootdir
, 'etc', 'network', 'interfaces.d', 'setup'), 'w') as eth
:
641 eth
.write('auto lo\n')
642 eth
.write('iface lo inet loopback\n')
644 if self
.settings
['enable-dhcp']:
646 eth
.write('auto eth0\n')
647 eth
.write('iface eth0 inet dhcp\n')
649 def append_serial_console(self
, rootdir
):
650 if self
.settings
['serial-console']:
651 serial_command
= self
.settings
['serial-console-command']
652 logging
.debug('adding getty to serial console')
653 inittab
= os
.path
.join(rootdir
, 'etc/inittab')
654 # to autologin, serial_command can contain '-a root'
655 with
open(inittab
, 'a') as f
:
656 f
.write('\nS0:23:respawn:%s\n' % serial_command
)
658 # pylint: disable=no-self-use
659 def _grub_serial_console(self
, rootdir
):
660 cmdline
= 'GRUB_CMDLINE_LINUX_DEFAULT="console=tty0 console=tty1 console=ttyS0,38400n8"'
661 terminal
= 'GRUB_TERMINAL="serial gfxterm"'
662 command
= 'GRUB_SERIAL_COMMAND="serial --speed=38400 --unit=0 --parity=no --stop=1"'
663 grub_cfg
= os
.path
.join(rootdir
, 'etc', 'default', 'grub')
664 logging
.debug("Allowing serial output in grub config %s", grub_cfg
)
665 with
open(grub_cfg
, 'a+') as cfg
:
666 cfg
.write("# %s serial support\n" % os
.path
.basename(__file__
))
667 cfg
.write("%s\n" % cmdline
)
668 cfg
.write("%s\n" % terminal
)
669 cfg
.write("%s\n" % command
)
671 def install_grub2(self
, rootdev
, rootdir
):
672 self
.message("Configuring grub2")
673 # rely on kpartx using consistent naming to map loop0p1 to loop0
674 install_dev
= os
.path
.join('/dev', os
.path
.basename(rootdev
)[:-2])
675 self
.runcmd(['mount', '/dev', '-t', 'devfs', '-obind',
676 '%s' % os
.path
.join(rootdir
, 'dev')])
677 self
.runcmd(['mount', '/proc', '-t', 'proc', '-obind',
678 '%s' % os
.path
.join(rootdir
, 'proc')])
679 self
.runcmd(['mount', '/sys', '-t', 'sysfs', '-obind',
680 '%s' % os
.path
.join(rootdir
, 'sys')])
681 if self
.settings
['serial-console']:
682 self
._grub
_serial
_console
(rootdir
)
685 self
.runcmd(['chroot', rootdir
, 'update-grub'])
686 self
.runcmd(['chroot', rootdir
, 'grub-install', install_dev
])
687 except cliapp
.AppException
:
688 self
.message("Failed. Is grub2-common installed? Using extlinux.")
689 self
.install_extlinux(rootdev
, rootdir
)
690 self
.runcmd(['umount', os
.path
.join(rootdir
, 'sys')])
691 self
.runcmd(['umount', os
.path
.join(rootdir
, 'proc')])
692 self
.runcmd(['umount', os
.path
.join(rootdir
, 'dev')])
694 def install_extlinux(self
, rootdev
, rootdir
):
695 if not os
.path
.exists("/usr/bin/extlinux"):
696 self
.message("extlinux not installed, skipping.")
698 self
.message('Installing extlinux')
701 dirname
= os
.path
.join(rootdir
, 'boot')
702 basenames
= os
.listdir(dirname
)
703 logging
.debug('find: %s', basenames
)
704 for basename
in basenames
:
705 if re
.search(pattern
, basename
):
706 return os
.path
.join('boot', basename
)
707 raise cliapp
.AppException('Cannot find match: %s' % pattern
)
710 kernel_image
= find('vmlinuz-.*')
711 initrd_image
= find('initrd.img-.*')
712 except cliapp
.AppException
as e
:
713 self
.message("Unable to find kernel. Not installing extlinux.")
714 logging
.debug("No kernel found. %s. Skipping install of extlinux.", e
)
717 out
= self
.runcmd(['blkid', '-c', '/dev/null', '-o', 'value',
718 '-s', 'UUID', rootdev
])
719 uuid
= out
.splitlines()[0].strip()
721 conf
= os
.path
.join(rootdir
, 'extlinux.conf')
722 logging
.debug('configure extlinux %s', conf
)
723 kserial
= 'console=ttyS0,115200' if self
.settings
['serial-console'] else ''
724 extserial
= 'serial 0 115200' if self
.settings
['serial-console'] else ''
731 append initrd=%(initrd)s root=UUID=%(uuid)s ro %(kserial)s
734 'kernel': kernel_image
, # pylint: disable=bad-continuation
735 'initrd': initrd_image
, # pylint: disable=bad-continuation
736 'uuid': uuid
, # pylint: disable=bad-continuation
737 'kserial': kserial
, # pylint: disable=bad-continuation
738 'extserial': extserial
, # pylint: disable=bad-continuation
739 } # pylint: disable=bad-continuation
740 logging
.debug("extlinux config:\n%s", msg
)
742 # python multiline string substitution is just ugly.
743 # use an external file or live with the mangling, no point in
744 # mangling the string to remove spaces just to keep it pretty in source.
748 self
.runcmd(['extlinux', '--install', rootdir
])
749 self
.runcmd(['sync'])
752 def optimize_image(self
, rootdir
):
754 Filing up the image with zeros will increase its compression rate
756 if not self
.settings
['sparse']:
757 zeros
= os
.path
.join(rootdir
, 'ZEROS')
758 self
.runcmd_unchecked(['dd', 'if=/dev/zero', 'of=' + zeros
, 'bs=1M'])
759 self
.runcmd(['rm', '-f', zeros
])
763 Run squashfs on the image.
765 if not os
.path
.exists('/usr/bin/mksquashfs'):
766 logging
.warning("Squash selected but mksquashfs not found!")
768 self
.message("Running mksquashfs")
769 suffixed
= "%s.squashfs" % self
.settings
['image']
770 self
.runcmd(['mksquashfs', self
.settings
['image'],
772 '-no-progress', '-comp', 'xz'], ignore_fail
=False)
773 os
.unlink(self
.settings
['image'])
774 self
.settings
['image'] = suffixed
776 def cleanup_system(self
):
777 # Clean up after any errors.
779 self
.message('Cleaning up')
781 # Umount in the reverse mount order
782 if self
.settings
['image']:
783 for i
in range(len(self
.mount_points
) - 1, -1, -1):
784 mount_point
= self
.mount_points
[i
]
786 self
.runcmd(['umount', mount_point
], ignore_fail
=False)
787 except cliapp
.AppException
:
788 logging
.debug("umount failed, sleeping and trying again")
790 self
.runcmd(['umount', mount_point
], ignore_fail
=False)
792 self
.runcmd(['kpartx', '-d', self
.settings
['image']], ignore_fail
=True)
794 for dirname
in self
.remove_dirs
:
795 shutil
.rmtree(dirname
)
797 def customize(self
, rootdir
):
798 script
= self
.settings
['customize']
801 if not os
.path
.exists(script
):
802 example
= os
.path
.join("/usr/share/vmdebootstrap/examples/", script
)
803 if not os
.path
.exists(example
):
804 self
.message("Unable to find %s" % script
)
807 self
.message('Running customize script %s' % script
)
808 logging
.info("rootdir=%s image=%s", rootdir
, self
.settings
['image'])
809 with
open('/dev/tty', 'w') as tty
:
811 cliapp
.runcmd([script
, rootdir
, self
.settings
['image']], stdout
=tty
, stderr
=tty
)
813 subprocess
.call([script
, rootdir
, self
.settings
['image']])
815 def create_tarball(self
, rootdir
):
816 # Create a tarball of the disk's contents
817 # shell out to runcmd since it more easily handles rootdir
818 self
.message('Creating tarball of disk contents')
819 self
.runcmd(['tar', '-cf', self
.settings
['tarball'], '-C', rootdir
, '.'])
822 # Change image owner after completed build
823 if self
.settings
['image']:
824 filename
= self
.settings
['image']
825 elif self
.settings
['tarball']:
826 filename
= self
.settings
['tarball']
829 self
.message("Changing owner to %s" % self
.settings
["owner"])
830 subprocess
.call(["chown", self
.settings
["owner"], filename
])
832 def list_installed_pkgs(self
, rootdir
):
833 # output the list of installed packages for sources identification
834 self
.message("Creating a list of installed binary package names")
835 out
= self
.runcmd(['chroot', rootdir
,
836 'dpkg-query', '-W', "-f='${Package}.deb\n'"])
837 with
open('dpkg.list', 'w') as dpkg
:
840 def configure_apt(self
, rootdir
):
841 # use the distribution and mirror to create an apt source
842 self
.message("Configuring apt to use distribution and mirror")
843 conf
= os
.path
.join(rootdir
, 'etc', 'apt', 'sources.list.d', 'base.list')
844 logging
.debug('configure apt %s', conf
)
845 mirror
= self
.settings
['mirror']
846 if self
.settings
['apt-mirror']:
847 mirror
= self
.settings
['apt-mirror']
848 self
.message("Setting apt mirror to %s" % mirror
)
849 os
.unlink(os
.path
.join(rootdir
, 'etc', 'apt', 'sources.list'))
851 line
= 'deb %s %s main\n' % (mirror
, self
.settings
['distribution'])
853 line
= '#deb-src %s %s main\n' % (mirror
, self
.settings
['distribution'])
856 # ensure the apt sources have valid lists
857 self
.runcmd(['chroot', rootdir
, 'apt-get', '-qq', 'update'])
859 if __name__
== '__main__':
860 VmDebootstrap(version
=__version__
).run()