]>
git.siccegge.de Git - forks/vmdebootstrap.git/blob - vmdebootstrap
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
):
321 out
= self
.runcmd(['kpartx', '-avs', self
.settings
['image']])
322 if self
.settings
['bootsize']:
330 devices
= [line
.split()[2]
331 for line
in out
.splitlines()
332 if line
.startswith('add map ')]
333 if len(devices
) != parts
:
334 raise cliapp
.AppException('Surprising number of partitions')
335 root
= '/dev/mapper/%s' % devices
[rootindex
]
336 if self
.settings
['bootsize']:
337 boot
= '/dev/mapper/%s' % devices
[bootindex
]
340 def mkfs(self
, device
, fstype
):
341 self
.message('Creating filesystem %s' % fstype
)
342 self
.runcmd(['mkfs', '-t', fstype
, device
])
344 def debootstrap(self
, rootdir
):
345 msg
= "(%s)" % self
.settings
['variant'] if self
.settings
['variant'] else ''
346 self
.message('Debootstrapping %s %s' % (self
.settings
['distribution'], msg
))
348 if self
.settings
['foreign']:
349 necessary_packages
= []
351 necessary_packages
= ['acpid']
353 if self
.settings
['grub']:
354 necessary_packages
.append('grub2')
356 include
= self
.settings
['package']
358 if not self
.settings
['no-kernel']:
359 if self
.settings
['arch'] == 'i386':
362 kernel_arch
= self
.settings
['arch']
363 kernel_image
= 'linux-image-%s' % kernel_arch
364 include
.append(kernel_image
)
366 if self
.settings
['sudo'] and 'sudo' not in include
:
367 include
.append('sudo')
369 args
= ['debootstrap', '--arch=%s' % self
.settings
['arch']]
370 if self
.settings
['package'] and len(necessary_packages
) > 0:
372 '--include=%s' % ','.join(necessary_packages
+ include
))
373 if self
.settings
['foreign']:
374 args
.append('--foreign')
375 if self
.settings
['variant']:
376 args
.append('--variant')
377 args
.append(self
.settings
['variant'])
378 args
+= [self
.settings
['distribution'],
379 rootdir
, self
.settings
['mirror']]
380 logging
.debug(" ".join(args
))
382 if self
.settings
['foreign']:
383 # set a noninteractive debconf environment for secondstage
385 "DEBIAN_FRONTEND": "noninteractive",
386 "DEBCONF_NONINTERACTIVE_SEEN": "true",
389 # add the mapping to the complete environment.
390 env
.update(os
.environ
)
391 # First copy the binfmt handler over
392 self
.message('Setting up binfmt handler')
393 shutil
.copy(self
.settings
['foreign'], '%s/usr/bin/' % rootdir
)
394 # Next, run the package install scripts etc.
395 self
.message('Running debootstrap second stage')
396 self
.runcmd(['chroot', rootdir
,
397 '/debootstrap/debootstrap', '--second-stage'],
400 def set_hostname(self
, rootdir
):
401 hostname
= self
.settings
['hostname']
402 with
open(os
.path
.join(rootdir
, 'etc', 'hostname'), 'w') as f
:
403 f
.write('%s\n' % hostname
)
405 etc_hosts
= os
.path
.join(rootdir
, 'etc', 'hosts')
407 with
open(etc_hosts
, 'r') as f
:
409 with
open(etc_hosts
, 'w') as f
:
410 for line
in data
.splitlines():
411 if line
.startswith('127.0.0.1'):
412 line
+= ' %s' % hostname
413 f
.write('%s\n' % line
)
417 def create_fstab(self
, rootdir
, rootdev
, roottype
, bootdev
, boottype
): # pylint: disable=too-many-arguments
419 out
= self
.runcmd(['blkid', '-c', '/dev/null', '-o', 'value',
420 '-s', 'UUID', device
])
421 return out
.splitlines()[0].strip()
424 rootdevstr
= 'UUID=%s' % fsuuid(rootdev
)
426 rootdevstr
= '/dev/sda1'
429 bootdevstr
= 'UUID=%s' % fsuuid(bootdev
)
433 fstab
= os
.path
.join(rootdir
, 'etc', 'fstab')
434 with
open(fstab
, 'w') as f
:
435 f
.write('proc /proc proc defaults 0 0\n')
436 f
.write('%s / %s errors=remount-ro 0 1\n' % (rootdevstr
, roottype
))
438 f
.write('%s /boot %s errors=remount-ro 0 2\n' % (bootdevstr
, boottype
))
440 def install_debs(self
, rootdir
):
441 if not self
.settings
['custom-package']:
443 self
.message('Installing custom packages')
444 tmp
= os
.path
.join(rootdir
, 'tmp', 'install_debs')
446 for deb
in self
.settings
['custom-package']:
447 shutil
.copy(deb
, tmp
)
448 filenames
= [os
.path
.join('/tmp/install_debs', os
.path
.basename(deb
))
449 for deb
in self
.settings
['custom-package']]
451 self
.runcmd_unchecked(['chroot', rootdir
, 'dpkg', '-i'] + filenames
)
452 logging
.debug('stdout:\n%s', out
)
453 logging
.debug('stderr:\n%s', err
)
454 out
= self
.runcmd(['chroot', rootdir
,
455 'apt-get', '-f', '--no-remove', 'install'])
456 logging
.debug('stdout:\n%s', out
)
459 def cleanup_apt_cache(self
, rootdir
):
460 out
= self
.runcmd(['chroot', rootdir
, 'apt-get', 'clean'])
461 logging
.debug('stdout:\n%s', out
)
463 def set_root_password(self
, rootdir
):
464 if self
.settings
['root-password']:
465 self
.message('Setting root password')
466 self
.set_password(rootdir
, 'root', self
.settings
['root-password'])
467 elif self
.settings
['lock-root-password']:
468 self
.message('Locking root password')
469 self
.runcmd(['chroot', rootdir
, 'passwd', '-l', 'root'])
471 self
.message('Give root an empty password')
472 self
.delete_password(rootdir
, 'root')
474 def create_users(self
, rootdir
):
475 def create_user(user
):
476 self
.runcmd(['chroot', rootdir
, 'adduser', '--gecos', user
,
477 '--disabled-password', user
])
478 if self
.settings
['sudo']:
479 self
.runcmd(['chroot', rootdir
, 'adduser', user
, 'sudo'])
481 for userpass
in self
.settings
['user']:
483 user
, password
= userpass
.split('/', 1)
485 self
.set_password(rootdir
, user
, password
)
487 create_user(userpass
)
488 self
.delete_password(rootdir
, userpass
)
490 def set_password(self
, rootdir
, user
, password
):
491 encrypted
= crypt
.crypt(password
, '..')
492 self
.runcmd(['chroot', rootdir
, 'usermod', '-p', encrypted
, user
])
494 def delete_password(self
, rootdir
, user
):
495 self
.runcmd(['chroot', rootdir
, 'passwd', '-d', user
])
497 def remove_udev_persistent_rules(self
, rootdir
):
498 self
.message('Removing udev persistent cd and net rules')
499 for x
in ['70-persistent-cd.rules', '70-persistent-net.rules']:
500 pathname
= os
.path
.join(rootdir
, 'etc', 'udev', 'rules.d', x
)
501 if os
.path
.exists(pathname
):
502 logging
.debug('rm %s', pathname
)
505 logging
.debug('not removing non-existent %s', pathname
)
507 def setup_networking(self
, rootdir
):
508 self
.message('Setting up networking')
510 f
= open(os
.path
.join(rootdir
, 'etc', 'network', 'interfaces'), 'w')
512 f
.write('iface lo inet loopback\n')
514 if self
.settings
['enable-dhcp']:
516 f
.write('auto eth0\n')
517 f
.write('iface eth0 inet dhcp\n')
521 def append_serial_console(self
, rootdir
):
522 if self
.settings
['serial-console']:
523 serial_command
= self
.settings
['serial-console-command']
524 logging
.debug('adding getty to serial console')
525 inittab
= os
.path
.join(rootdir
, 'etc/inittab')
526 # to autologin, serial_command can contain '-a root'
527 with
open(inittab
, 'a') as f
:
528 f
.write('\nS0:23:respawn:%s\n' % serial_command
)
530 def install_grub2(self
, rootdev
, rootdir
):
531 self
.message("Configuring grub2")
532 # rely on kpartx using consistent naming to map loop0p1 to loop0
533 install_dev
= os
.path
.join('/dev', os
.path
.basename(rootdev
)[:-2])
534 self
.runcmd(['mount', '/dev', '-t', 'devfs', '-obind',
535 '%s' % os
.path
.join(rootdir
, 'dev')])
536 self
.runcmd(['mount', '/proc', '-t', 'proc', '-obind',
537 '%s' % os
.path
.join(rootdir
, 'proc')])
538 self
.runcmd(['mount', '/sys', '-t', 'sysfs', '-obind',
539 '%s' % os
.path
.join(rootdir
, 'sys')])
541 self
.runcmd(['chroot', rootdir
, 'update-grub'])
542 self
.runcmd(['chroot', rootdir
, 'grub-install', install_dev
])
543 except cliapp
.AppException
:
544 self
.message("Failed. Is grub2-common installed? Using extlinux.")
545 self
.runcmd(['umount', os
.path
.join(rootdir
, 'sys')])
546 self
.runcmd(['umount', os
.path
.join(rootdir
, 'proc')])
547 self
.runcmd(['umount', os
.path
.join(rootdir
, 'dev')])
548 self
.install_extlinux(rootdev
, rootdir
)
550 def install_extlinux(self
, rootdev
, rootdir
):
551 if not os
.path
.exists("/usr/bin/extlinux"):
552 self
.message("extlinux not installed, skipping.")
554 self
.message('Installing extlinux')
557 dirname
= os
.path
.join(rootdir
, 'boot')
558 basenames
= os
.listdir(dirname
)
559 logging
.debug('find: %s', basenames
)
560 for basename
in basenames
:
561 if re
.search(pattern
, basename
):
562 return os
.path
.join('boot', basename
)
563 raise cliapp
.AppException('Cannot find match: %s' % pattern
)
566 kernel_image
= find('vmlinuz-.*')
567 initrd_image
= find('initrd.img-.*')
568 except cliapp
.AppException
as e
:
569 self
.message("Unable to find kernel. Not installing extlinux.")
570 logging
.debug("No kernel found. %s. Skipping install of extlinux.", e
)
573 out
= self
.runcmd(['blkid', '-c', '/dev/null', '-o', 'value',
574 '-s', 'UUID', rootdev
])
575 uuid
= out
.splitlines()[0].strip()
577 conf
= os
.path
.join(rootdir
, 'extlinux.conf')
578 logging
.debug('configure extlinux %s', conf
)
579 # python multiline string substitution is just ugly.
580 # use an external file or live with the mangling, no point in
581 # mangling the string to remove spaces just to keep it pretty in source.
589 append initrd=%(initrd)s root=UUID=%(uuid)s ro %(kserial)s
592 'kernel': kernel_image
, # pylint: disable=bad-continuation
593 'initrd': initrd_image
, # pylint: disable=bad-continuation
594 'uuid': uuid
, # pylint: disable=bad-continuation
595 'kserial': # pylint: disable=bad-continuation
596 'console=ttyS0,115200' if self
.settings
['serial-console'] else '', # pylint: disable=bad-continuation
597 'extserial': 'serial 0 115200' if self
.settings
['serial-console'] else '', # pylint: disable=bad-continuation
598 }) # pylint: disable=bad-continuation
599 f
.close() # pylint: disable=bad-continuation
601 self
.runcmd(['extlinux', '--install', rootdir
])
602 self
.runcmd(['sync'])
605 def optimize_image(self
, rootdir
):
607 Filing up the image with zeros will increase its compression rate
609 if not self
.settings
['sparse']:
610 zeros
= os
.path
.join(rootdir
, 'ZEROS')
611 self
.runcmd_unchecked(['dd', 'if=/dev/zero', 'of=' + zeros
, 'bs=1M'])
612 self
.runcmd(['rm', '-f', zeros
])
616 Run squashfs on the image.
618 if not os
.path
.exists('/usr/bin/mksquashfs'):
619 logging
.warning("Squash selected but mksquashfs not found!")
621 self
.message("Running mksquashfs")
622 suffixed
= "%s.squashfs" % self
.settings
['image']
623 self
.runcmd(['mksquashfs', self
.settings
['image'],
625 '-no-progress', '-comp', 'xz'], ignore_fail
=False)
626 os
.unlink(self
.settings
['image'])
627 self
.settings
['image'] = suffixed
629 def cleanup_system(self
):
630 # Clean up after any errors.
632 self
.message('Cleaning up')
634 # Umount in the reverse mount order
635 if self
.settings
['image']:
636 for i
in range(len(self
.mount_points
) - 1, -1, -1):
637 mount_point
= self
.mount_points
[i
]
639 self
.runcmd(['umount', mount_point
], ignore_fail
=False)
640 except cliapp
.AppException
:
641 logging
.debug("umount failed, sleeping and trying again")
643 self
.runcmd(['umount', mount_point
], ignore_fail
=False)
645 self
.runcmd(['kpartx', '-d', self
.settings
['image']], ignore_fail
=True)
647 for dirname
in self
.remove_dirs
:
648 shutil
.rmtree(dirname
)
650 def customize(self
, rootdir
):
651 script
= self
.settings
['customize']
654 if not os
.path
.exists(script
):
655 example
= os
.path
.join("/usr/share/vmdebootstrap/examples/", script
)
656 if not os
.path
.exists(example
):
657 self
.message("Unable to find %s" % script
)
660 self
.message('Running customize script %s' % script
)
661 with
open('/dev/tty', 'w') as tty
:
662 cliapp
.runcmd([script
, rootdir
], stdout
=tty
, stderr
=tty
)
664 def create_tarball(self
, rootdir
):
665 # Create a tarball of the disk's contents
666 # shell out to runcmd since it more easily handles rootdir
667 self
.message('Creating tarball of disk contents')
668 self
.runcmd(['tar', '-cf', self
.settings
['tarball'], '-C', rootdir
, '.'])
671 # Change image owner after completed build
672 self
.message("Changing owner to %s" % self
.settings
["owner"])
673 subprocess
.call(["chown",
674 self
.settings
["owner"],
675 self
.settings
["image"]])
677 def list_installed_pkgs(self
, rootdir
):
678 # output the list of installed packages for sources identification
679 self
.message("Creating a list of installed binary package names")
680 out
= self
.runcmd(['chroot', rootdir
,
681 'dpkg-query', '-W' "-f='${Package}.deb\n'"])
682 with
open('dpkg.list', 'w') as dpkg
:
685 def configure_apt(self
, rootdir
):
686 # use the distribution and mirror to create an apt source
687 self
.message("Configuring apt to use distribution and mirror")
688 conf
= os
.path
.join(rootdir
, 'etc', 'apt', 'sources.list.d', 'base.list')
689 logging
.debug('configure apt %s', conf
)
690 mirror
= self
.settings
['mirror']
691 if self
.settings
['apt-mirror']:
692 mirror
= self
.settings
['apt-mirror']
693 self
.message("Setting apt mirror to %s" % mirror
)
694 os
.unlink(os
.path
.join(rootdir
, 'etc', 'apt', 'sources.list'))
696 line
= 'deb %s %s main\n' % (mirror
, self
.settings
['distribution'])
698 line
= '#deb-src %s %s main\n' % (mirror
, self
.settings
['distribution'])
702 if __name__
== '__main__':
703 VmDebootstrap(version
=__version__
).run()