]>
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/>.
33 class VmDebootstrap(cliapp
.Application
):
35 def add_settings(self
):
36 default_arch
= subprocess
.check_output(
37 ["dpkg", "--print-architecture"]).strip()
39 self
.settings
.boolean(['verbose'], 'report what is going on')
40 self
.settings
.string(['image'], 'put created disk image in FILE',
42 self
.settings
.bytesize(['size'],
43 'create a disk image of size SIZE (%default)',
46 self
.settings
.bytesize(['bootsize'],
47 'create boot partition of size SIZE (%default)',
50 self
.settings
.string(['boottype'],
51 'specify file system type for /boot/',
53 self
.settings
.string(['foreign'],
54 'set up foreign debootstrap environment using provided program (ie binfmt handler)')
55 self
.settings
.string(['variant'],
56 'select debootstrap variant it not using the default')
57 self
.settings
.boolean(['extlinux'], 'install extlinux?', default
=True)
58 self
.settings
.string(['tarball'], "tar up the disk's contents in FILE",
60 self
.settings
.string(['apt-mirror'],
61 'configure apt to use MIRROR',
63 self
.settings
.string(['mirror'],
64 'use MIRROR as package source (%default)',
66 default
='http://http.debian.net/debian/')
67 self
.settings
.string(['arch'], 'architecture to use (%default)',
70 self
.settings
.string(['distribution'],
71 'release to use (%default)',
74 self
.settings
.string_list(['package'], 'install PACKAGE onto system')
75 self
.settings
.string_list(['custom-package'],
76 'install package in DEB file onto system '
79 self
.settings
.boolean(['no-kernel'], 'do not install a linux package')
80 self
.settings
.boolean(['enable-dhcp'], 'enable DHCP on eth0')
81 self
.settings
.string(['root-password'], 'set root password',
83 self
.settings
.boolean(['lock-root-password'],
84 'lock root account so they cannot login?')
85 self
.settings
.string(['customize'],
86 'run SCRIPT after setting up system',
88 self
.settings
.string(['hostname'],
89 'set name to HOSTNAME (%default)',
92 self
.settings
.string_list(['user'],
93 'create USER with PASSWORD',
94 metavar
='USER/PASSWORD')
95 self
.settings
.boolean(['serial-console'],
96 'configure image to use a serial console')
97 self
.settings
.string(['serial-console-command'],
98 'command to manage the serial console, appended '
99 'to /etc/inittab (%default)',
101 default
='/sbin/getty -L ttyS0 115200 vt100')
102 self
.settings
.boolean(['sudo'],
103 'install sudo, and if user is created, add them '
105 self
.settings
.string(['owner'],
106 'the user who will own the image when the build '
108 self
.settings
.boolean(['squash'],
109 'use squashfs on the final image.')
110 self
.settings
.boolean(['configure-apt'],
111 'Create an apt source based on the distribution '
112 'and mirror selected.')
113 self
.settings
.boolean(['mbr'],
114 'Run install-mbr (no longer done by default)')
115 self
.settings
.boolean(['grub'],
116 'Install and configure grub2 - disables '
118 self
.settings
.boolean(['sparse'],
119 'Dont fill the image with zeros to keep a sparse disk image',
121 self
.settings
.boolean(['pkglist'],
122 'Create a list of package names included in '
124 self
.remove_dirs
= []
125 self
.mount_points
= []
127 def process_args(self
, args
):
128 if not self
.settings
['image'] and not self
.settings
['tarball']:
129 raise cliapp
.AppException('You must give disk image filename, '
130 'or tarball filename')
131 if self
.settings
['image'] and not self
.settings
['size']:
132 raise cliapp
.AppException('If disk image is specified, '
133 'You must give image size.')
135 self
.remove_dirs
= []
136 self
.mount_points
= []
144 if self
.settings
['image']:
145 self
.create_empty_image()
146 self
.partition_image()
147 if self
.settings
['mbr']:
149 (rootdev
, bootdev
) = self
.setup_kpartx()
150 self
.mkfs(rootdev
, fstype
=roottype
)
151 rootdir
= self
.mount(rootdev
)
153 if self
.settings
['boottype']:
154 boottype
= self
.settings
['boottype']
157 self
.mkfs(bootdev
, fstype
=boottype
)
158 bootdir
= '%s/%s' % (rootdir
, 'boot/')
160 bootdir
= self
.mount(bootdev
, bootdir
)
162 rootdir
= self
.mkdtemp()
163 self
.debootstrap(rootdir
)
164 self
.set_hostname(rootdir
)
165 self
.create_fstab(rootdir
, rootdev
, roottype
, bootdev
, boottype
)
166 self
.install_debs(rootdir
)
167 self
.cleanup_apt_cache(rootdir
)
168 self
.set_root_password(rootdir
)
169 self
.create_users(rootdir
)
170 self
.remove_udev_persistent_rules(rootdir
)
171 self
.setup_networking(rootdir
)
172 if self
.settings
['configure-apt'] or self
.settings
['apt-mirror']:
173 self
.configure_apt(rootdir
)
174 self
.customize(rootdir
)
175 self
.update_initramfs(rootdir
)
177 if self
.settings
['image']:
178 if self
.settings
['grub']:
179 self
.install_grub2(rootdev
, rootdir
)
180 elif self
.settings
['extlinux']:
181 self
.install_extlinux(rootdev
, rootdir
)
182 self
.append_serial_console(rootdir
)
183 self
.optimize_image(rootdir
)
184 if self
.settings
['squash']:
186 if self
.settings
['pkglist']:
187 self
.list_installed_pkgs(rootdir
)
189 if self
.settings
['foreign']:
190 os
.unlink('%s/usr/bin/%s' %
191 (rootdir
, os
.path
.basename(self
.settings
['foreign'])))
193 if self
.settings
['tarball']:
194 self
.create_tarball(rootdir
)
196 if self
.settings
['owner']:
198 except BaseException
, e
:
199 self
.message('EEEK! Something bad happened...')
201 db_log
= os
.path
.join(rootdir
, 'debootstrap', 'debootstrap.log')
202 if os
.path
.exists(db_log
):
203 shutil
.copy(db_log
, os
.getcwd())
205 self
.cleanup_system()
208 self
.cleanup_system()
210 def message(self
, msg
):
212 if self
.settings
['verbose']:
215 def runcmd(self
, argv
, stdin
='', ignore_fail
=False, env
=None, **kwargs
):
216 logging
.debug('runcmd: %s %s %s', argv
, env
, kwargs
)
217 p
= subprocess
.Popen(argv
, stdin
=subprocess
.PIPE
,
218 stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
,
220 out
, err
= p
.communicate(stdin
)
221 if p
.returncode
!= 0:
222 msg
= 'command failed: %s\n%s\n%s' % (argv
, out
, err
)
225 raise cliapp
.AppException(msg
)
229 dirname
= tempfile
.mkdtemp()
230 self
.remove_dirs
.append(dirname
)
231 logging
.debug('mkdir %s', dirname
)
234 def mount(self
, device
, path
=None):
236 mount_point
= self
.mkdtemp()
239 self
.message('Mounting %s on %s' % (device
, mount_point
))
240 self
.runcmd(['mount', device
, mount_point
])
241 self
.mount_points
.append(mount_point
)
242 logging
.debug('mounted %s on %s', device
, mount_point
)
245 def create_empty_image(self
):
246 self
.message('Creating disk image')
247 self
.runcmd(['qemu-img', 'create', '-f', 'raw',
248 self
.settings
['image'],
249 str(self
.settings
['size'])])
251 def partition_image(self
):
252 self
.message('Creating partitions')
253 self
.runcmd(['parted', '-s', self
.settings
['image'],
255 if self
.settings
['bootsize'] and self
.settings
['bootsize'] is not '0%':
256 bootsize
= str(self
.settings
['bootsize'] / (1024 * 1024))
257 self
.runcmd(['parted', '-s', self
.settings
['image'],
258 'mkpart', 'primary', 'fat16', '0%', bootsize
])
261 self
.runcmd(['parted', '-s', self
.settings
['image'],
262 'mkpart', 'primary', bootsize
, '100%'])
263 self
.runcmd(['parted', '-s', self
.settings
['image'],
264 'set', '1', 'boot', 'on'])
266 def update_initramfs(self
, rootdir
):
267 cmd
= os
.path
.join('usr', 'sbin', 'update-initramfs')
268 if os
.path
.exists(os
.path
.join(rootdir
, cmd
)):
269 self
.message("Updating the initramfs")
270 self
.runcmd(['chroot', rootdir
, cmd
, '-u'])
272 def install_mbr(self
):
273 if os
.path
.exists("/sbin/install-mbr"):
274 self
.message('Installing MBR')
275 self
.runcmd(['install-mbr', self
.settings
['image']])
277 def setup_kpartx(self
):
278 out
= self
.runcmd(['kpartx', '-avs', self
.settings
['image']])
279 if self
.settings
['bootsize']:
287 devices
= [line
.split()[2]
288 for line
in out
.splitlines()
289 if line
.startswith('add map ')]
290 if len(devices
) != parts
:
291 raise cliapp
.AppException('Surprising number of partitions')
292 root
= '/dev/mapper/%s' % devices
[rootindex
]
293 if self
.settings
['bootsize']:
294 boot
= '/dev/mapper/%s' % devices
[bootindex
]
297 def mkfs(self
, device
, fstype
):
298 self
.message('Creating filesystem %s' % fstype
)
299 self
.runcmd(['mkfs', '-t', fstype
, device
])
301 def debootstrap(self
, rootdir
):
302 msg
= "(%s)" % self
.settings
['variant'] if self
.settings
['variant'] else ''
303 self
.message('Debootstrapping %s %s' % (self
.settings
['distribution'], msg
))
305 if self
.settings
['foreign']:
306 necessary_packages
= []
308 necessary_packages
= ['acpid']
310 if self
.settings
['grub']:
311 necessary_packages
.append('grub2')
313 include
= self
.settings
['package']
315 if not self
.settings
['no-kernel']:
316 if self
.settings
['arch'] == 'i386':
319 kernel_arch
= self
.settings
['arch']
320 kernel_image
= 'linux-image-%s' % kernel_arch
321 include
.append(kernel_image
)
323 if self
.settings
['sudo'] and 'sudo' not in include
:
324 include
.append('sudo')
326 args
= ['debootstrap', '--arch=%s' % self
.settings
['arch']]
327 if self
.settings
['package'] and len(necessary_packages
) > 0:
329 '--include=%s' % ','.join(necessary_packages
+ include
))
330 if self
.settings
['foreign']:
331 args
.append('--foreign')
332 if self
.settings
['variant']:
333 args
.append('--variant')
334 args
.append(self
.settings
['variant'])
335 args
+= [self
.settings
['distribution'],
336 rootdir
, self
.settings
['mirror']]
337 logging
.debug(" ".join(args
))
339 if self
.settings
['foreign']:
340 # set a noninteractive debconf environment for secondstage
342 "DEBIAN_FRONTEND": "noninteractive",
343 "DEBCONF_NONINTERACTIVE_SEEN": "true",
346 # add the mapping to the complete environment.
347 env
.update(os
.environ
)
348 # First copy the binfmt handler over
349 self
.message('Setting up binfmt handler')
350 shutil
.copy(self
.settings
['foreign'], '%s/usr/bin/' % rootdir
)
351 # Next, run the package install scripts etc.
352 self
.message('Running debootstrap second stage')
353 self
.runcmd(['chroot', rootdir
,
354 '/debootstrap/debootstrap', '--second-stage'],
357 def set_hostname(self
, rootdir
):
358 hostname
= self
.settings
['hostname']
359 with
open(os
.path
.join(rootdir
, 'etc', 'hostname'), 'w') as f
:
360 f
.write('%s\n' % hostname
)
362 etc_hosts
= os
.path
.join(rootdir
, 'etc', 'hosts')
364 with
open(etc_hosts
, 'r') as f
:
366 with
open(etc_hosts
, 'w') as f
:
367 for line
in data
.splitlines():
368 if line
.startswith('127.0.0.1'):
369 line
+= ' %s' % hostname
370 f
.write('%s\n' % line
)
374 def create_fstab(self
, rootdir
, rootdev
, roottype
, bootdev
, boottype
):
376 out
= self
.runcmd(['blkid', '-c', '/dev/null', '-o', 'value',
377 '-s', 'UUID', device
])
378 return out
.splitlines()[0].strip()
381 rootdevstr
= 'UUID=%s' % fsuuid(rootdev
)
383 rootdevstr
= '/dev/sda1'
386 bootdevstr
= 'UUID=%s' % fsuuid(bootdev
)
390 fstab
= os
.path
.join(rootdir
, 'etc', 'fstab')
391 with
open(fstab
, 'w') as f
:
392 f
.write('proc /proc proc defaults 0 0\n')
393 f
.write('%s / %s errors=remount-ro 0 1\n' % (rootdevstr
, roottype
))
395 f
.write('%s /boot %s errors=remount-ro 0 2\n' % (bootdevstr
, boottype
))
397 def install_debs(self
, rootdir
):
398 if not self
.settings
['custom-package']:
400 self
.message('Installing custom packages')
401 tmp
= os
.path
.join(rootdir
, 'tmp', 'install_debs')
403 for deb
in self
.settings
['custom-package']:
404 shutil
.copy(deb
, tmp
)
405 filenames
= [os
.path
.join('/tmp/install_debs', os
.path
.basename(deb
))
406 for deb
in self
.settings
['custom-package']]
407 out
, err
, exitcode
= \
408 self
.runcmd_unchecked(['chroot', rootdir
, 'dpkg', '-i'] + filenames
)
409 logging
.debug('stdout:\n%s', out
)
410 logging
.debug('stderr:\n%s', err
)
411 out
= self
.runcmd(['chroot', rootdir
,
412 'apt-get', '-f', '--no-remove', 'install'])
413 logging
.debug('stdout:\n%s', out
)
416 def cleanup_apt_cache(self
, rootdir
):
417 out
= self
.runcmd(['chroot', rootdir
, 'apt-get', 'clean'])
418 logging
.debug('stdout:\n%s', out
)
420 def set_root_password(self
, rootdir
):
421 if self
.settings
['root-password']:
422 self
.message('Setting root password')
423 self
.set_password(rootdir
, 'root', self
.settings
['root-password'])
424 elif self
.settings
['lock-root-password']:
425 self
.message('Locking root password')
426 self
.runcmd(['chroot', rootdir
, 'passwd', '-l', 'root'])
428 self
.message('Give root an empty password')
429 self
.delete_password(rootdir
, 'root')
431 def create_users(self
, rootdir
):
432 def create_user(user
):
433 self
.runcmd(['chroot', rootdir
, 'adduser', '--gecos', user
,
434 '--disabled-password', user
])
435 if self
.settings
['sudo']:
436 self
.runcmd(['chroot', rootdir
, 'adduser', user
, 'sudo'])
438 for userpass
in self
.settings
['user']:
440 user
, password
= userpass
.split('/', 1)
442 self
.set_password(rootdir
, user
, password
)
444 create_user(userpass
)
445 self
.delete_password(rootdir
, userpass
)
447 def set_password(self
, rootdir
, user
, password
):
448 encrypted
= crypt
.crypt(password
, '..')
449 self
.runcmd(['chroot', rootdir
, 'usermod', '-p', encrypted
, user
])
451 def delete_password(self
, rootdir
, user
):
452 self
.runcmd(['chroot', rootdir
, 'passwd', '-d', user
])
454 def remove_udev_persistent_rules(self
, rootdir
):
455 self
.message('Removing udev persistent cd and net rules')
456 for x
in ['70-persistent-cd.rules', '70-persistent-net.rules']:
457 pathname
= os
.path
.join(rootdir
, 'etc', 'udev', 'rules.d', x
)
458 if os
.path
.exists(pathname
):
459 logging
.debug('rm %s', pathname
)
462 logging
.debug('not removing non-existent %s', pathname
)
464 def setup_networking(self
, rootdir
):
465 self
.message('Setting up networking')
467 f
= open(os
.path
.join(rootdir
, 'etc', 'network', 'interfaces'), 'w')
469 f
.write('iface lo inet loopback\n')
471 if self
.settings
['enable-dhcp']:
473 f
.write('auto eth0\n')
474 f
.write('iface eth0 inet dhcp\n')
478 def append_serial_console(self
, rootdir
):
479 if self
.settings
['serial-console']:
480 serial_command
= self
.settings
['serial-console-command']
481 logging
.debug('adding getty to serial console')
482 inittab
= os
.path
.join(rootdir
, 'etc/inittab')
483 # to autologin, serial_command can contain '-a root'
484 with
open(inittab
, 'a') as f
:
485 f
.write('\nS0:23:respawn:%s\n' % serial_command
)
487 def install_grub2(self
, rootdev
, rootdir
):
488 self
.message("Configuring grub2")
489 # rely on kpartx using consistent naming to map loop0p1 to loop0
490 install_dev
= os
.path
.join('/dev', os
.path
.basename(rootdev
)[:-2])
491 self
.runcmd(['mount', '/dev', '-t', 'devfs', '-obind',
492 '%s' % os
.path
.join(rootdir
, 'dev')])
493 self
.runcmd(['mount', '/proc', '-t', 'proc', '-obind',
494 '%s' % os
.path
.join(rootdir
, 'proc')])
495 self
.runcmd(['mount', '/sys', '-t', 'sysfs', '-obind',
496 '%s' % os
.path
.join(rootdir
, 'sys')])
498 self
.runcmd(['chroot', rootdir
, 'update-grub'])
499 self
.runcmd(['chroot', rootdir
, 'grub-install', install_dev
])
500 except cliapp
.AppException
as e
:
501 self
.message("Failed. Is grub2-common installed? Using extlinux.")
502 self
.runcmd(['umount', os
.path
.join(rootdir
, 'sys')])
503 self
.runcmd(['umount', os
.path
.join(rootdir
, 'proc')])
504 self
.runcmd(['umount', os
.path
.join(rootdir
, 'dev')])
505 self
.install_extlinux(rootdev
, rootdir
)
507 def install_extlinux(self
, rootdev
, rootdir
):
508 if not os
.path
.exists("/usr/bin/extlinux"):
509 self
.message("extlinux not installed, skipping.")
511 self
.message('Installing extlinux')
514 dirname
= os
.path
.join(rootdir
, 'boot')
515 basenames
= os
.listdir(dirname
)
516 logging
.debug('find: %s', basenames
)
517 for basename
in basenames
:
518 if re
.search(pattern
, basename
):
519 return os
.path
.join('boot', basename
)
520 raise cliapp
.AppException('Cannot find match: %s' % pattern
)
523 kernel_image
= find('vmlinuz-.*')
524 initrd_image
= find('initrd.img-.*')
525 except cliapp
.AppException
as e
:
526 self
.message("Unable to find kernel. Not installing extlinux.")
527 logging
.debug("No kernel found. %s. Skipping install of extlinux.", e
)
530 out
= self
.runcmd(['blkid', '-c', '/dev/null', '-o', 'value',
531 '-s', 'UUID', rootdev
])
532 uuid
= out
.splitlines()[0].strip()
534 conf
= os
.path
.join(rootdir
, 'extlinux.conf')
535 logging
.debug('configure extlinux %s', conf
)
543 append initrd=%(initrd)s root=UUID=%(uuid)s ro %(kserial)s
546 'kernel': kernel_image
,
547 'initrd': initrd_image
,
550 'console=ttyS0,115200' if self
.settings
['serial-console'] else '',
551 'extserial': 'serial 0 115200' if self
.settings
['serial-console'] else '',
555 self
.runcmd(['extlinux', '--install', rootdir
])
556 self
.runcmd(['sync'])
559 def optimize_image(self
, rootdir
):
561 Filing up the image with zeros will increase its compression rate
563 if not self
.settings
['sparse']:
564 zeros
= os
.path
.join(rootdir
, 'ZEROS')
565 self
.runcmd_unchecked(['dd', 'if=/dev/zero', 'of=' + zeros
, 'bs=1M'])
566 self
.runcmd(['rm', '-f', zeros
])
570 Run squashfs on the image.
572 if not os
.path
.exists('/usr/bin/mksquashfs'):
573 logging
.warning("Squash selected but mksquashfs not found!")
575 self
.message("Running mksquashfs")
576 suffixed
= "%s.squashfs" % self
.settings
['image']
577 self
.runcmd(['mksquashfs', self
.settings
['image'],
579 '-no-progress', '-comp', 'xz'], ignore_fail
=False)
580 os
.unlink(self
.settings
['image'])
581 self
.settings
['image'] = suffixed
583 def cleanup_system(self
):
584 # Clean up after any errors.
586 self
.message('Cleaning up')
588 # Umount in the reverse mount order
589 if self
.settings
['image']:
590 for i
in xrange(len(self
.mount_points
) - 1, -1, -1):
591 mount_point
= self
.mount_points
[i
]
593 self
.runcmd(['umount', mount_point
], ignore_fail
=False)
594 except cliapp
.AppException
:
595 logging
.debug("umount failed, sleeping and trying again")
597 self
.runcmd(['umount', mount_point
], ignore_fail
=False)
599 self
.runcmd(['kpartx', '-d', self
.settings
['image']], ignore_fail
=True)
601 for dirname
in self
.remove_dirs
:
602 shutil
.rmtree(dirname
)
604 def customize(self
, rootdir
):
605 script
= self
.settings
['customize']
608 if not os
.path
.exists(script
):
609 example
= os
.path
.join("/usr/share/vmdebootstrap/examples/", script
)
610 if not os
.path
.exists(example
):
611 self
.message("Unable to find %s" % script
)
614 self
.message('Running customize script %s' % script
)
615 with
open('/dev/tty', 'w') as tty
:
616 cliapp
.runcmd([script
, rootdir
], stdout
=tty
, stderr
=tty
)
618 def create_tarball(self
, rootdir
):
619 # Create a tarball of the disk's contents
620 # shell out to runcmd since it more easily handles rootdir
621 self
.message('Creating tarball of disk contents')
622 self
.runcmd(['tar', '-cf', self
.settings
['tarball'], '-C', rootdir
, '.'])
624 def chown(self
, rootdir
):
625 # Change image owner after completed build
626 self
.message("Changing owner to %s" % self
.settings
["owner"])
627 subprocess
.call(["chown",
628 self
.settings
["owner"],
629 self
.settings
["image"]])
631 def list_installed_pkgs(self
, rootdir
):
632 # output the list of installed packages for sources identification
633 self
.message("Creating a list of installed binary package names")
634 out
= self
.runcmd(['chroot', rootdir
,
635 'dpkg-query', '-W' "-f='${Package}.deb\n'"])
636 with
open('dpkg.list', 'w') as dpkg
:
639 def configure_apt(self
, rootdir
):
640 # use the distribution and mirror to create an apt source
641 self
.message("Configuring apt to use distribution and mirror")
642 conf
= os
.path
.join(rootdir
, 'etc', 'apt', 'sources.list.d', 'base.list')
643 logging
.debug('configure apt %s', conf
)
644 mirror
= self
.settings
['mirror']
645 if self
.settings
['apt-mirror']:
646 mirror
= self
.settings
['apt-mirror']
647 self
.message("Setting apt mirror to %s" % mirror
)
648 os
.unlink(os
.path
.join(rootdir
, 'etc', 'apt', 'sources.list'))
651 deb %(mirror)s %(distribution)s main
652 #deb-src %(mirror)s %(distribution)s main
655 'distribution': self
.settings
['distribution']
659 if __name__
== '__main__':
660 VmDebootstrap(version
=__version__
).run()