]>
git.siccegge.de Git - forks/vmdebootstrap.git/blob - vmdebootstrap
aae437a9120e5ceaab66e7a18fab1913badd9eea
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/>.
32 # pylint: disable=invalid-name
35 class VmDebootstrap(cliapp
.Application
): # pylint: disable=too-many-public-methods
37 def __init__(self
, progname
=None, version
=__version__
, description
=None, epilog
=None):
38 super(VmDebootstrap
, self
).__init
__(progname
, version
, description
, epilog
)
40 self
.mount_points
= []
42 def add_settings(self
):
43 default_arch
= subprocess
.check_output(
44 ["dpkg", "--print-architecture"]).strip()
46 self
.settings
.boolean(
47 ['verbose'], 'report what is going on')
49 ['image'], 'put created disk image in FILE',
51 self
.settings
.bytesize(
53 'create a disk image of size SIZE (%default)',
56 self
.settings
.bytesize(
58 'create boot partition of size SIZE (%default)',
63 'specify file system type for /boot/',
65 self
.settings
.bytesize(
67 'Space to leave at start of the image for bootloader',
71 'Partition type to use for this image',
75 'set up foreign debootstrap environment using provided program (ie binfmt handler)')
78 'select debootstrap variant it not using the default')
79 self
.settings
.boolean(
85 "tar up the disk's contents in FILE",
89 'configure apt to use MIRROR',
93 'use MIRROR as package source (%default)',
95 default
='http://http.debian.net/debian/')
98 'architecture to use (%default)',
100 default
=default_arch
)
101 self
.settings
.string(
103 'release to use (%default)',
106 self
.settings
.string_list(
108 'install PACKAGE onto system')
109 self
.settings
.string_list(
111 'install package in DEB file onto system (not from mirror)',
113 self
.settings
.boolean(
115 'do not install a linux package')
116 self
.settings
.boolean(
118 'enable DHCP on eth0')
119 self
.settings
.string(
123 self
.settings
.boolean(
124 ['lock-root-password'],
125 'lock root account so they cannot login?')
126 self
.settings
.string(
128 'run SCRIPT after setting up system',
130 self
.settings
.string(
132 'set name to HOSTNAME (%default)',
135 self
.settings
.string_list(
137 'create USER with PASSWORD',
138 metavar
='USER/PASSWORD')
139 self
.settings
.boolean(
141 'configure image to use a serial console')
142 self
.settings
.string(
143 ['serial-console-command'],
144 'command to manage the serial console, appended to /etc/inittab (%default)',
146 default
='/sbin/getty -L ttyS0 115200 vt100')
147 self
.settings
.boolean(
149 'install sudo, and if user is created, add them to sudo group')
150 self
.settings
.string(
152 'the user who will own the image when the build is complete.')
153 self
.settings
.boolean(
155 'use squashfs on the final image.')
156 self
.settings
.boolean(
158 'Create an apt source based on the distribution and mirror selected.')
159 self
.settings
.boolean(
161 'Run install-mbr (no longer done by default)')
162 self
.settings
.boolean(
164 'Install and configure grub2 - disables extlinux.')
165 self
.settings
.boolean(
167 'Dont fill the image with zeros to keep a sparse disk image',
169 self
.settings
.boolean(
171 'Create a list of package names included in the image.')
173 def process_args(self
, args
): # pylint: disable=too-many-branches,too-many-statements
174 if not self
.settings
['image'] and not self
.settings
['tarball']:
175 raise cliapp
.AppException(
176 'You must give disk image filename, or tarball filename')
177 if self
.settings
['image'] and not self
.settings
['size']:
178 raise cliapp
.AppException(
179 'If disk image is specified, you must give image size.')
187 if self
.settings
['image']:
188 self
.create_empty_image()
189 self
.partition_image()
190 if self
.settings
['mbr']:
192 (rootdev
, bootdev
) = self
.setup_kpartx()
193 self
.mkfs(rootdev
, fstype
=roottype
)
194 rootdir
= self
.mount(rootdev
)
196 if self
.settings
['boottype']:
197 boottype
= self
.settings
['boottype']
200 self
.mkfs(bootdev
, fstype
=boottype
)
201 bootdir
= '%s/%s' % (rootdir
, 'boot/')
203 self
.mount(bootdev
, bootdir
)
205 rootdir
= self
.mkdtemp()
206 self
.debootstrap(rootdir
)
207 self
.set_hostname(rootdir
)
208 self
.create_fstab(rootdir
, rootdev
, roottype
, bootdev
, boottype
)
209 self
.install_debs(rootdir
)
210 self
.cleanup_apt_cache(rootdir
)
211 self
.set_root_password(rootdir
)
212 self
.create_users(rootdir
)
213 self
.remove_udev_persistent_rules(rootdir
)
214 self
.setup_networking(rootdir
)
215 if self
.settings
['configure-apt'] or self
.settings
['apt-mirror']:
216 self
.configure_apt(rootdir
)
217 self
.customize(rootdir
)
218 self
.update_initramfs(rootdir
)
220 if self
.settings
['image']:
221 if self
.settings
['grub']:
222 self
.install_grub2(rootdev
, rootdir
)
223 elif self
.settings
['extlinux']:
224 self
.install_extlinux(rootdev
, rootdir
)
225 self
.append_serial_console(rootdir
)
226 self
.optimize_image(rootdir
)
227 if self
.settings
['squash']:
229 if self
.settings
['pkglist']:
230 self
.list_installed_pkgs(rootdir
)
232 if self
.settings
['foreign']:
233 os
.unlink('%s/usr/bin/%s' %
234 (rootdir
, os
.path
.basename(self
.settings
['foreign'])))
236 if self
.settings
['tarball']:
237 self
.create_tarball(rootdir
)
239 if self
.settings
['owner']:
241 except BaseException
, e
:
242 self
.message('EEEK! Something bad happened...')
244 db_log
= os
.path
.join(rootdir
, 'debootstrap', 'debootstrap.log')
245 if os
.path
.exists(db_log
):
246 shutil
.copy(db_log
, os
.getcwd())
248 self
.cleanup_system()
251 self
.cleanup_system()
253 def message(self
, msg
):
255 if self
.settings
['verbose']:
258 def runcmd(self
, argv
, stdin
='', ignore_fail
=False, env
=None, **kwargs
):
259 logging
.debug('runcmd: %s %s %s', argv
, env
, kwargs
)
260 p
= subprocess
.Popen(argv
, stdin
=subprocess
.PIPE
,
261 stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
,
263 out
, err
= p
.communicate(stdin
)
264 if p
.returncode
!= 0:
265 msg
= 'command failed: %s\n%s\n%s' % (argv
, out
, err
)
268 raise cliapp
.AppException(msg
)
272 dirname
= tempfile
.mkdtemp()
273 self
.remove_dirs
.append(dirname
)
274 logging
.debug('mkdir %s', dirname
)
277 def mount(self
, device
, path
=None):
279 mount_point
= self
.mkdtemp()
282 self
.message('Mounting %s on %s' % (device
, mount_point
))
283 self
.runcmd(['mount', device
, mount_point
])
284 self
.mount_points
.append(mount_point
)
285 logging
.debug('mounted %s on %s', device
, mount_point
)
288 def create_empty_image(self
):
289 self
.message('Creating disk image')
290 self
.runcmd(['qemu-img', 'create', '-f', 'raw',
291 self
.settings
['image'],
292 str(self
.settings
['size'])])
294 def partition_image(self
):
295 self
.message('Creating partitions')
296 self
.runcmd(['parted', '-s', self
.settings
['image'],
298 if self
.settings
['bootsize'] and self
.settings
['bootsize'] is not '0%':
299 bootsize
= str(self
.settings
['bootsize'] / (1024 * 1024))
300 self
.runcmd(['parted', '-s', self
.settings
['image'],
301 'mkpart', 'primary', 'fat16', '0%', bootsize
])
304 self
.runcmd(['parted', '-s', self
.settings
['image'],
305 'mkpart', 'primary', bootsize
, '100%'])
306 self
.runcmd(['parted', '-s', self
.settings
['image'],
307 'set', '1', 'boot', 'on'])
309 def update_initramfs(self
, rootdir
):
310 cmd
= os
.path
.join('usr', 'sbin', 'update-initramfs')
311 if os
.path
.exists(os
.path
.join(rootdir
, cmd
)):
312 self
.message("Updating the initramfs")
313 self
.runcmd(['chroot', rootdir
, cmd
, '-u'])
315 def install_mbr(self
):
316 if os
.path
.exists("/sbin/install-mbr"):
317 self
.message('Installing MBR')
318 self
.runcmd(['install-mbr', self
.settings
['image']])
320 def setup_kpartx(self
):
322 out
= self
.runcmd(['kpartx', '-avs', self
.settings
['image']])
323 if self
.settings
['bootsize']:
331 devices
= [line
.split()[2]
332 for line
in out
.splitlines()
333 if line
.startswith('add map ')]
334 if len(devices
) != parts
:
335 raise cliapp
.AppException('Surprising number of partitions')
336 root
= '/dev/mapper/%s' % devices
[rootindex
]
337 if self
.settings
['bootsize']:
338 boot
= '/dev/mapper/%s' % devices
[bootindex
]
341 def mkfs(self
, device
, fstype
):
342 self
.message('Creating filesystem %s' % fstype
)
343 self
.runcmd(['mkfs', '-t', fstype
, device
])
345 def debootstrap(self
, rootdir
):
346 msg
= "(%s)" % self
.settings
['variant'] if self
.settings
['variant'] else ''
347 self
.message('Debootstrapping %s %s' % (self
.settings
['distribution'], msg
))
349 if self
.settings
['foreign']:
350 necessary_packages
= []
352 necessary_packages
= ['acpid']
354 if self
.settings
['grub']:
355 necessary_packages
.append('grub2')
357 include
= self
.settings
['package']
359 if not self
.settings
['no-kernel']:
360 if self
.settings
['arch'] == 'i386':
363 kernel_arch
= self
.settings
['arch']
364 kernel_image
= 'linux-image-%s' % kernel_arch
365 include
.append(kernel_image
)
367 if self
.settings
['sudo'] and 'sudo' not in include
:
368 include
.append('sudo')
370 args
= ['debootstrap', '--arch=%s' % self
.settings
['arch']]
371 if self
.settings
['package']:
373 '--include=%s' % ','.join(include
))
374 if len(necessary_packages
) > 0:
376 '--include=%s' % ','.join(necessary_packages
))
377 if self
.settings
['foreign']:
378 args
.append('--foreign')
379 if self
.settings
['variant']:
380 args
.append('--variant')
381 args
.append(self
.settings
['variant'])
382 args
+= [self
.settings
['distribution'],
383 rootdir
, self
.settings
['mirror']]
384 logging
.debug(" ".join(args
))
386 if self
.settings
['foreign']:
387 # set a noninteractive debconf environment for secondstage
389 "DEBIAN_FRONTEND": "noninteractive",
390 "DEBCONF_NONINTERACTIVE_SEEN": "true",
393 # add the mapping to the complete environment.
394 env
.update(os
.environ
)
395 # First copy the binfmt handler over
396 self
.message('Setting up binfmt handler')
397 shutil
.copy(self
.settings
['foreign'], '%s/usr/bin/' % rootdir
)
398 # Next, run the package install scripts etc.
399 self
.message('Running debootstrap second stage')
400 self
.runcmd(['chroot', rootdir
,
401 '/debootstrap/debootstrap', '--second-stage'],
404 def set_hostname(self
, rootdir
):
405 hostname
= self
.settings
['hostname']
406 with
open(os
.path
.join(rootdir
, 'etc', 'hostname'), 'w') as f
:
407 f
.write('%s\n' % hostname
)
409 etc_hosts
= os
.path
.join(rootdir
, 'etc', 'hosts')
411 with
open(etc_hosts
, 'r') as f
:
413 with
open(etc_hosts
, 'w') as f
:
414 for line
in data
.splitlines():
415 if line
.startswith('127.0.0.1'):
416 line
+= ' %s' % hostname
417 f
.write('%s\n' % line
)
421 def create_fstab(self
, rootdir
, rootdev
, roottype
, bootdev
, boottype
): # pylint: disable=too-many-arguments
423 out
= self
.runcmd(['blkid', '-c', '/dev/null', '-o', 'value',
424 '-s', 'UUID', device
])
425 return out
.splitlines()[0].strip()
428 rootdevstr
= 'UUID=%s' % fsuuid(rootdev
)
430 rootdevstr
= '/dev/sda1'
433 bootdevstr
= 'UUID=%s' % fsuuid(bootdev
)
437 fstab
= os
.path
.join(rootdir
, 'etc', 'fstab')
438 with
open(fstab
, 'w') as f
:
439 f
.write('proc /proc proc defaults 0 0\n')
440 f
.write('%s / %s errors=remount-ro 0 1\n' % (rootdevstr
, roottype
))
442 f
.write('%s /boot %s errors=remount-ro 0 2\n' % (bootdevstr
, boottype
))
444 def install_debs(self
, rootdir
):
445 if not self
.settings
['custom-package']:
447 self
.message('Installing custom packages')
448 tmp
= os
.path
.join(rootdir
, 'tmp', 'install_debs')
450 for deb
in self
.settings
['custom-package']:
451 shutil
.copy(deb
, tmp
)
452 filenames
= [os
.path
.join('/tmp/install_debs', os
.path
.basename(deb
))
453 for deb
in self
.settings
['custom-package']]
455 self
.runcmd_unchecked(['chroot', rootdir
, 'dpkg', '-i'] + filenames
)
456 logging
.debug('stdout:\n%s', out
)
457 logging
.debug('stderr:\n%s', err
)
458 out
= self
.runcmd(['chroot', rootdir
,
459 'apt-get', '-f', '--no-remove', 'install'])
460 logging
.debug('stdout:\n%s', out
)
463 def cleanup_apt_cache(self
, rootdir
):
464 out
= self
.runcmd(['chroot', rootdir
, 'apt-get', 'clean'])
465 logging
.debug('stdout:\n%s', out
)
467 def set_root_password(self
, rootdir
):
468 if self
.settings
['root-password']:
469 self
.message('Setting root password')
470 self
.set_password(rootdir
, 'root', self
.settings
['root-password'])
471 elif self
.settings
['lock-root-password']:
472 self
.message('Locking root password')
473 self
.runcmd(['chroot', rootdir
, 'passwd', '-l', 'root'])
475 self
.message('Give root an empty password')
476 self
.delete_password(rootdir
, 'root')
478 def create_users(self
, rootdir
):
479 def create_user(user
):
480 self
.runcmd(['chroot', rootdir
, 'adduser', '--gecos', user
,
481 '--disabled-password', user
])
482 if self
.settings
['sudo']:
483 self
.runcmd(['chroot', rootdir
, 'adduser', user
, 'sudo'])
485 for userpass
in self
.settings
['user']:
487 user
, password
= userpass
.split('/', 1)
489 self
.set_password(rootdir
, user
, password
)
491 create_user(userpass
)
492 self
.delete_password(rootdir
, userpass
)
494 def set_password(self
, rootdir
, user
, password
):
495 encrypted
= crypt
.crypt(password
, '..')
496 self
.runcmd(['chroot', rootdir
, 'usermod', '-p', encrypted
, user
])
498 def delete_password(self
, rootdir
, user
):
499 self
.runcmd(['chroot', rootdir
, 'passwd', '-d', user
])
501 def remove_udev_persistent_rules(self
, rootdir
):
502 self
.message('Removing udev persistent cd and net rules')
503 for x
in ['70-persistent-cd.rules', '70-persistent-net.rules']:
504 pathname
= os
.path
.join(rootdir
, 'etc', 'udev', 'rules.d', x
)
505 if os
.path
.exists(pathname
):
506 logging
.debug('rm %s', pathname
)
509 logging
.debug('not removing non-existent %s', pathname
)
511 def setup_networking(self
, rootdir
):
512 self
.message('Setting up networking')
514 f
= open(os
.path
.join(rootdir
, 'etc', 'network', 'interfaces'), 'w')
516 f
.write('iface lo inet loopback\n')
518 if self
.settings
['enable-dhcp']:
520 f
.write('auto eth0\n')
521 f
.write('iface eth0 inet dhcp\n')
525 def append_serial_console(self
, rootdir
):
526 if self
.settings
['serial-console']:
527 serial_command
= self
.settings
['serial-console-command']
528 logging
.debug('adding getty to serial console')
529 inittab
= os
.path
.join(rootdir
, 'etc/inittab')
530 # to autologin, serial_command can contain '-a root'
531 with
open(inittab
, 'a') as f
:
532 f
.write('\nS0:23:respawn:%s\n' % serial_command
)
534 def install_grub2(self
, rootdev
, rootdir
):
535 self
.message("Configuring grub2")
536 # rely on kpartx using consistent naming to map loop0p1 to loop0
537 install_dev
= os
.path
.join('/dev', os
.path
.basename(rootdev
)[:-2])
538 self
.runcmd(['mount', '/dev', '-t', 'devfs', '-obind',
539 '%s' % os
.path
.join(rootdir
, 'dev')])
540 self
.runcmd(['mount', '/proc', '-t', 'proc', '-obind',
541 '%s' % os
.path
.join(rootdir
, 'proc')])
542 self
.runcmd(['mount', '/sys', '-t', 'sysfs', '-obind',
543 '%s' % os
.path
.join(rootdir
, 'sys')])
545 self
.runcmd(['chroot', rootdir
, 'update-grub'])
546 self
.runcmd(['chroot', rootdir
, 'grub-install', install_dev
])
547 except cliapp
.AppException
:
548 self
.message("Failed. Is grub2-common installed? Using extlinux.")
549 self
.runcmd(['umount', os
.path
.join(rootdir
, 'sys')])
550 self
.runcmd(['umount', os
.path
.join(rootdir
, 'proc')])
551 self
.runcmd(['umount', os
.path
.join(rootdir
, 'dev')])
552 self
.install_extlinux(rootdev
, rootdir
)
554 def install_extlinux(self
, rootdev
, rootdir
):
555 if not os
.path
.exists("/usr/bin/extlinux"):
556 self
.message("extlinux not installed, skipping.")
558 self
.message('Installing extlinux')
561 dirname
= os
.path
.join(rootdir
, 'boot')
562 basenames
= os
.listdir(dirname
)
563 logging
.debug('find: %s', basenames
)
564 for basename
in basenames
:
565 if re
.search(pattern
, basename
):
566 return os
.path
.join('boot', basename
)
567 raise cliapp
.AppException('Cannot find match: %s' % pattern
)
570 kernel_image
= find('vmlinuz-.*')
571 initrd_image
= find('initrd.img-.*')
572 except cliapp
.AppException
as e
:
573 self
.message("Unable to find kernel. Not installing extlinux.")
574 logging
.debug("No kernel found. %s. Skipping install of extlinux.", e
)
577 out
= self
.runcmd(['blkid', '-c', '/dev/null', '-o', 'value',
578 '-s', 'UUID', rootdev
])
579 uuid
= out
.splitlines()[0].strip()
581 conf
= os
.path
.join(rootdir
, 'extlinux.conf')
582 logging
.debug('configure extlinux %s', conf
)
583 # python multiline string substitution is just ugly.
584 # use an external file or live with the mangling, no point in
585 # mangling the string to remove spaces just to keep it pretty in source.
593 append initrd=%(initrd)s root=UUID=%(uuid)s ro %(kserial)s
596 'kernel': kernel_image
, # pylint: disable=bad-continuation
597 'initrd': initrd_image
, # pylint: disable=bad-continuation
598 'uuid': uuid
, # pylint: disable=bad-continuation
599 'kserial': # pylint: disable=bad-continuation
600 'console=ttyS0,115200' if self
.settings
['serial-console'] else '', # pylint: disable=bad-continuation
601 'extserial': 'serial 0 115200' if self
.settings
['serial-console'] else '', # pylint: disable=bad-continuation
602 }) # pylint: disable=bad-continuation
603 f
.close() # pylint: disable=bad-continuation
605 self
.runcmd(['extlinux', '--install', rootdir
])
606 self
.runcmd(['sync'])
609 def optimize_image(self
, rootdir
):
611 Filing up the image with zeros will increase its compression rate
613 if not self
.settings
['sparse']:
614 zeros
= os
.path
.join(rootdir
, 'ZEROS')
615 self
.runcmd_unchecked(['dd', 'if=/dev/zero', 'of=' + zeros
, 'bs=1M'])
616 self
.runcmd(['rm', '-f', zeros
])
620 Run squashfs on the image.
622 if not os
.path
.exists('/usr/bin/mksquashfs'):
623 logging
.warning("Squash selected but mksquashfs not found!")
625 self
.message("Running mksquashfs")
626 suffixed
= "%s.squashfs" % self
.settings
['image']
627 self
.runcmd(['mksquashfs', self
.settings
['image'],
629 '-no-progress', '-comp', 'xz'], ignore_fail
=False)
630 os
.unlink(self
.settings
['image'])
631 self
.settings
['image'] = suffixed
633 def cleanup_system(self
):
634 # Clean up after any errors.
636 self
.message('Cleaning up')
638 # Umount in the reverse mount order
639 if self
.settings
['image']:
640 for i
in range(len(self
.mount_points
) - 1, -1, -1):
641 mount_point
= self
.mount_points
[i
]
643 self
.runcmd(['umount', mount_point
], ignore_fail
=False)
644 except cliapp
.AppException
:
645 logging
.debug("umount failed, sleeping and trying again")
647 self
.runcmd(['umount', mount_point
], ignore_fail
=False)
649 self
.runcmd(['kpartx', '-d', self
.settings
['image']], ignore_fail
=True)
651 for dirname
in self
.remove_dirs
:
652 shutil
.rmtree(dirname
)
654 def customize(self
, rootdir
):
655 script
= self
.settings
['customize']
658 if not os
.path
.exists(script
):
659 example
= os
.path
.join("/usr/share/vmdebootstrap/examples/", script
)
660 if not os
.path
.exists(example
):
661 self
.message("Unable to find %s" % script
)
664 self
.message('Running customize script %s' % script
)
665 with
open('/dev/tty', 'w') as tty
:
666 cliapp
.runcmd([script
, rootdir
], stdout
=tty
, stderr
=tty
)
668 def create_tarball(self
, rootdir
):
669 # Create a tarball of the disk's contents
670 # shell out to runcmd since it more easily handles rootdir
671 self
.message('Creating tarball of disk contents')
672 self
.runcmd(['tar', '-cf', self
.settings
['tarball'], '-C', rootdir
, '.'])
675 # Change image owner after completed build
676 self
.message("Changing owner to %s" % self
.settings
["owner"])
677 subprocess
.call(["chown",
678 self
.settings
["owner"],
679 self
.settings
["image"]])
681 def list_installed_pkgs(self
, rootdir
):
682 # output the list of installed packages for sources identification
683 self
.message("Creating a list of installed binary package names")
684 out
= self
.runcmd(['chroot', rootdir
,
685 'dpkg-query', '-W' "-f='${Package}.deb\n'"])
686 with
open('dpkg.list', 'w') as dpkg
:
689 def configure_apt(self
, rootdir
):
690 # use the distribution and mirror to create an apt source
691 self
.message("Configuring apt to use distribution and mirror")
692 conf
= os
.path
.join(rootdir
, 'etc', 'apt', 'sources.list.d', 'base.list')
693 logging
.debug('configure apt %s', conf
)
694 mirror
= self
.settings
['mirror']
695 if self
.settings
['apt-mirror']:
696 mirror
= self
.settings
['apt-mirror']
697 self
.message("Setting apt mirror to %s" % mirror
)
698 os
.unlink(os
.path
.join(rootdir
, 'etc', 'apt', 'sources.list'))
700 line
= 'deb %s %s main\n' % (mirror
, self
.settings
['distribution'])
702 line
= '#deb-src %s %s main\n' % (mirror
, self
.settings
['distribution'])
706 if __name__
== '__main__':
707 VmDebootstrap(version
=__version__
).run()