]> git.siccegge.de Git - forks/vmdebootstrap.git/blob - vmdebootstrap
Run update-initramfs -u after installing kernel
[forks/vmdebootstrap.git] / vmdebootstrap
1 #!/usr/bin/python
2 # Copyright 2011-2013 Lars Wirzenius
3 # Copyright 2012 Codethink Limited
4 # Copyright 2014 Neil Williams <codehelp@debian.org>
5 #
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.
10 #
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.
15 #
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/>.
18
19 import cliapp
20 import crypt
21 import logging
22 import os
23 import re
24 import shutil
25 import subprocess
26 import tempfile
27 import time
28
29
30 __version__ = '0.4'
31
32
33 class VmDebootstrap(cliapp.Application):
34
35 def add_settings(self):
36 default_arch = subprocess.check_output(
37 ["dpkg", "--print-architecture"]).strip()
38
39 self.settings.boolean(['verbose'], 'report what is going on')
40 self.settings.string(['image'], 'put created disk image in FILE',
41 metavar='FILE')
42 self.settings.bytesize(['size'],
43 'create a disk image of size SIZE (%default)',
44 metavar='SIZE',
45 default='1G')
46 self.settings.bytesize(['bootsize'],
47 'create boot partition of size SIZE (%default)',
48 metavar='BOOTSIZE',
49 default='0%')
50 self.settings.string(['boottype'],
51 'specify file system type for /boot/',
52 default='ext2')
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",
59 metavar='FILE')
60 self.settings.string(['mirror'],
61 'use MIRROR as package source (%default)',
62 metavar='URL',
63 default='http://cdn.debian.net/debian/')
64 self.settings.string(['arch'], 'architecture to use (%default)',
65 metavar='ARCH',
66 default=default_arch)
67 self.settings.string(['distribution'],
68 'release to use (%default)',
69 metavar='NAME',
70 default='stable')
71 self.settings.string_list(['package'], 'install PACKAGE onto system')
72 self.settings.string_list(['custom-package'],
73 'install package in DEB file onto system '
74 '(not from mirror)',
75 metavar='DEB')
76 self.settings.boolean(['no-kernel'], 'do not install a linux package')
77 self.settings.boolean(['enable-dhcp'], 'enable DHCP on eth0')
78 self.settings.string(['root-password'], 'set root password',
79 metavar='PASSWORD')
80 self.settings.boolean(['lock-root-password'],
81 'lock root account so they cannot login?')
82 self.settings.string(['customize'],
83 'run SCRIPT after setting up system',
84 metavar='SCRIPT')
85 self.settings.string(['hostname'],
86 'set name to HOSTNAME (%default)',
87 metavar='HOSTNAME',
88 default='debian')
89 self.settings.string_list(['user'],
90 'create USER with PASSWORD',
91 metavar='USER/PASSWORD')
92 self.settings.boolean(['serial-console'],
93 'configure image to use a serial console')
94 self.settings.string(['serial-console-command'],
95 'command to manage the serial console, appended '
96 'to /etc/inittab (%default)',
97 metavar='COMMAND',
98 default='/sbin/getty -L ttyS0 115200 vt100')
99 self.settings.boolean(['sudo'],
100 'install sudo, and if user is created, add them '
101 'to sudo group')
102 self.settings.string(['owner'],
103 'the user who will own the image when the build '
104 'is complete.')
105 self.settings.boolean(['squash'],
106 'use squashfs on the final image.')
107 self.settings.boolean(['configure-apt'],
108 'Create an apt source based on the distribution '
109 'and mirror selected.')
110 self.settings.boolean(['mbr'],
111 'Run install-mbr (no longer done by default)')
112 self.settings.boolean(['grub'],
113 'Install and configure grub2 - disables '
114 'extlinux.')
115
116 def process_args(self, args):
117 if not self.settings['image'] and not self.settings['tarball']:
118 raise cliapp.AppException('You must give disk image filename, '
119 'or tarball filename')
120 if self.settings['image'] and not self.settings['size']:
121 raise cliapp.AppException('If disk image is specified, '
122 'You must give image size.')
123
124 self.remove_dirs = []
125 self.mount_points = []
126
127 try:
128 rootdev = None
129 roottype = 'ext4'
130 bootdev = None
131 boottype = None
132 rootdir = None
133 if self.settings['image']:
134 self.create_empty_image()
135 self.partition_image()
136 if self.settings['mbr']:
137 self.install_mbr()
138 (rootdev, bootdev) = self.setup_kpartx()
139 self.mkfs(rootdev, type=roottype)
140 rootdir = self.mount(rootdev)
141 if bootdev:
142 if self.settings['boottype']:
143 boottype = self.settings['boottype']
144 else:
145 boottype = 'ext2'
146 self.mkfs(bootdev, type=boottype)
147 bootdir = '%s/%s' % (rootdir, 'boot/')
148 os.mkdir(bootdir)
149 bootdir = self.mount(bootdev, bootdir)
150 else:
151 rootdir = self.mkdtemp()
152 self.debootstrap(rootdir)
153 self.set_hostname(rootdir)
154 self.create_fstab(rootdir, rootdev, roottype, bootdev, boottype)
155 self.install_debs(rootdir)
156 self.cleanup_apt_cache(rootdir)
157 self.set_root_password(rootdir)
158 self.create_users(rootdir)
159 self.remove_udev_persistent_rules(rootdir)
160 self.setup_networking(rootdir)
161 if self.settings['configure-apt']:
162 self.configure_apt(rootdir)
163 self.customize(rootdir)
164 self.update_initramfs(rootdir)
165
166 if self.settings['image']:
167 if self.settings['grub']:
168 self.install_grub2(rootdev, rootdir)
169 elif self.settings['extlinux']:
170 self.install_extlinux(rootdev, rootdir)
171 self.append_serial_console(rootdir)
172 self.optimize_image(rootdir)
173 if self.settings['squash']:
174 self.squash()
175
176 if self.settings['foreign']:
177 os.unlink('%s/usr/bin/%s' %
178 (rootdir, os.path.basename(self.settings['foreign'])))
179
180 if self.settings['tarball']:
181 self.create_tarball(rootdir)
182
183 if self.settings['owner']:
184 self.chown(rootdir)
185 except BaseException, e:
186 self.message('EEEK! Something bad happened...')
187 if rootdir:
188 db_log = os.path.join(rootdir, 'debootstrap', 'debootstrap.log')
189 if os.path.exists(db_log):
190 shutil.copy(db_log, os.getcwd())
191 self.message(e)
192 self.cleanup_system()
193 raise
194 else:
195 self.cleanup_system()
196
197 def message(self, msg):
198 logging.info(msg)
199 if self.settings['verbose']:
200 print msg
201
202 def runcmd(self, argv, stdin='', ignore_fail=False, env=None, **kwargs):
203 logging.debug('runcmd: %s %s %s' % (argv, env, kwargs))
204 p = subprocess.Popen(argv, stdin=subprocess.PIPE,
205 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
206 env=env, **kwargs)
207 out, err = p.communicate(stdin)
208 if p.returncode != 0:
209 msg = 'command failed: %s\n%s\n%s' % (argv, out, err)
210 logging.error(msg)
211 if not ignore_fail:
212 raise cliapp.AppException(msg)
213 return out
214
215 def mkdtemp(self):
216 dirname = tempfile.mkdtemp()
217 self.remove_dirs.append(dirname)
218 logging.debug('mkdir %s' % dirname)
219 return dirname
220
221 def mount(self, device, path=None):
222 if not path:
223 mount_point = self.mkdtemp()
224 else:
225 mount_point = path
226 self.message('Mounting %s on %s' % (device, mount_point))
227 self.runcmd(['mount', device, mount_point])
228 self.mount_points.append(mount_point)
229 logging.debug('mounted %s on %s' % (device, mount_point))
230 return mount_point
231
232 def create_empty_image(self):
233 self.message('Creating disk image')
234 self.runcmd(['qemu-img', 'create', '-f', 'raw',
235 self.settings['image'],
236 str(self.settings['size'])])
237
238 def partition_image(self):
239 self.message('Creating partitions')
240 self.runcmd(['parted', '-s', self.settings['image'],
241 'mklabel', 'msdos'])
242 if self.settings['bootsize'] and self.settings['bootsize'] is not '0%':
243 bootsize = str(self.settings['bootsize'] / (1024 * 1024))
244 self.runcmd(['parted', '-s', self.settings['image'],
245 'mkpart', 'primary', 'fat16', '0', bootsize])
246 else:
247 bootsize = '0%'
248 self.runcmd(['parted', '-s', self.settings['image'],
249 'mkpart', 'primary', bootsize, '100%'])
250 self.runcmd(['parted', '-s', self.settings['image'],
251 'set', '1', 'boot', 'on'])
252
253 def update_initramfs(self, rootdir):
254 cmd = os.path.join('usr', 'sbin', 'update-initramfs')
255 if os.path.exists(os.path.join(rootdir, cmd)):
256 self.message("Updating the initramfs")
257 self.runcmd(['chroot', rootdir, cmd, '-u'])
258
259 def install_mbr(self):
260 if os.path.exists("/sbin/install-mbr"):
261 self.message('Installing MBR')
262 self.runcmd(['install-mbr', self.settings['image']])
263
264 def setup_kpartx(self):
265 out = self.runcmd(['kpartx', '-avs', self.settings['image']])
266 if self.settings['bootsize']:
267 bootindex = 0
268 rootindex = 1
269 parts = 2
270 else:
271 rootindex = 0
272 parts = 1
273 boot = None
274 devices = [line.split()[2]
275 for line in out.splitlines()
276 if line.startswith('add map ')]
277 if len(devices) != parts:
278 raise cliapp.AppException('Surprising number of partitions')
279 root = '/dev/mapper/%s' % devices[rootindex]
280 if self.settings['bootsize']:
281 boot = '/dev/mapper/%s' % devices[bootindex]
282 return (root, boot)
283
284 def mkfs(self, device, type):
285 self.message('Creating filesystem %s' % type)
286 self.runcmd(['mkfs', '-t', type, device])
287
288 def debootstrap(self, rootdir):
289 self.message('Debootstrapping')
290
291 if self.settings['foreign']:
292 necessary_packages = []
293 else:
294 necessary_packages = ['acpid']
295
296 if self.settings['grub']:
297 necessary_packages.append('grub2')
298
299 include = self.settings['package']
300
301 if not self.settings['no-kernel']:
302 if self.settings['arch'] == 'i386':
303 kernel_arch = '486'
304 else:
305 kernel_arch = self.settings['arch']
306 kernel_image = 'linux-image-%s' % kernel_arch
307 include.append(kernel_image)
308
309 if self.settings['sudo'] and 'sudo' not in include:
310 include.append('sudo')
311
312 args = ['debootstrap', '--arch=%s' % self.settings['arch']]
313 if self.settings['package'] and len(necessary_packages) > 0:
314 args.append(
315 '--include=%s' % ','.join(necessary_packages + include))
316 if self.settings['foreign']:
317 args.append('--foreign')
318 if self.settings['variant']:
319 args.append('--variant')
320 args.append(self.settings['variant'])
321 args += [self.settings['distribution'],
322 rootdir, self.settings['mirror']]
323 logging.debug(" ".join(args))
324 self.runcmd(args)
325 if self.settings['foreign']:
326 # set a noninteractive debconf environment for secondstage
327 env = {
328 "DEBIAN_FRONTEND": "noninteractive",
329 "DEBCONF_NONINTERACTIVE_SEEN": "true",
330 "LC_ALL": "C"
331 }
332 # add the mapping to the complete environment.
333 env.update(os.environ)
334 # First copy the binfmt handler over
335 self.message('Setting up binfmt handler')
336 shutil.copy(self.settings['foreign'], '%s/usr/bin/' % rootdir)
337 # Next, run the package install scripts etc.
338 self.message('Running debootstrap second stage')
339 self.runcmd(['chroot', rootdir,
340 '/debootstrap/debootstrap', '--second-stage'],
341 env=env)
342
343 def set_hostname(self, rootdir):
344 hostname = self.settings['hostname']
345 with open(os.path.join(rootdir, 'etc', 'hostname'), 'w') as f:
346 f.write('%s\n' % hostname)
347
348 etc_hosts = os.path.join(rootdir, 'etc', 'hosts')
349 try:
350 with open(etc_hosts, 'r') as f:
351 data = f.read()
352 with open(etc_hosts, 'w') as f:
353 for line in data.splitlines():
354 if line.startswith('127.0.0.1'):
355 line += ' %s' % hostname
356 f.write('%s\n' % line)
357 except IOError, e:
358 pass
359
360 def create_fstab(self, rootdir, rootdev, roottype, bootdev, boottype):
361 def fsuuid(device):
362 out = self.runcmd(['blkid', '-c', '/dev/null', '-o', 'value',
363 '-s', 'UUID', device])
364 return out.splitlines()[0].strip()
365
366 if rootdev:
367 rootdevstr = 'UUID=%s' % fsuuid(rootdev)
368 else:
369 rootdevstr = '/dev/sda1'
370
371 if bootdev:
372 bootdevstr = 'UUID=%s' % fsuuid(bootdev)
373 else:
374 bootdevstr = None
375
376 fstab = os.path.join(rootdir, 'etc', 'fstab')
377 with open(fstab, 'w') as f:
378 f.write('proc /proc proc defaults 0 0\n')
379 f.write('%s / %s errors=remount-ro 0 1\n' % (rootdevstr, roottype))
380 if bootdevstr:
381 f.write('%s /boot %s errors=remount-ro 0 2\n' % (bootdevstr, boottype))
382
383 def install_debs(self, rootdir):
384 if not self.settings['custom-package']:
385 return
386 self.message('Installing custom packages')
387 tmp = os.path.join(rootdir, 'tmp', 'install_debs')
388 os.mkdir(tmp)
389 for deb in self.settings['custom-package']:
390 shutil.copy(deb, tmp)
391 filenames = [os.path.join('/tmp/install_debs', os.path.basename(deb))
392 for deb in self.settings['custom-package']]
393 out, err, exit = \
394 self.runcmd_unchecked(['chroot', rootdir, 'dpkg', '-i'] + filenames)
395 logging.debug('stdout:\n%s' % out)
396 logging.debug('stderr:\n%s' % err)
397 out = self.runcmd(['chroot', rootdir,
398 'apt-get', '-f', '--no-remove', 'install'])
399 logging.debug('stdout:\n%s' % out)
400 shutil.rmtree(tmp)
401
402 def cleanup_apt_cache(self, rootdir):
403 out = self.runcmd(['chroot', rootdir, 'apt-get', 'clean'])
404 logging.debug('stdout:\n%s' % out)
405
406 def set_root_password(self, rootdir):
407 if self.settings['root-password']:
408 self.message('Setting root password')
409 self.set_password(rootdir, 'root', self.settings['root-password'])
410 elif self.settings['lock-root-password']:
411 self.message('Locking root password')
412 self.runcmd(['chroot', rootdir, 'passwd', '-l', 'root'])
413 else:
414 self.message('Give root an empty password')
415 self.delete_password(rootdir, 'root')
416
417 def create_users(self, rootdir):
418 def create_user(user):
419 self.runcmd(['chroot', rootdir, 'adduser', '--gecos', user,
420 '--disabled-password', user])
421 if self.settings['sudo']:
422 self.runcmd(['chroot', rootdir, 'adduser', user, 'sudo'])
423
424 for userpass in self.settings['user']:
425 if '/' in userpass:
426 user, password = userpass.split('/', 1)
427 create_user(user)
428 self.set_password(rootdir, user, password)
429 else:
430 create_user(userpass)
431 self.delete_password(rootdir, userpass)
432
433 def set_password(self, rootdir, user, password):
434 encrypted = crypt.crypt(password, '..')
435 self.runcmd(['chroot', rootdir, 'usermod', '-p', encrypted, user])
436
437 def delete_password(self, rootdir, user):
438 self.runcmd(['chroot', rootdir, 'passwd', '-d', user])
439
440 def remove_udev_persistent_rules(self, rootdir):
441 self.message('Removing udev persistent cd and net rules')
442 for x in ['70-persistent-cd.rules', '70-persistent-net.rules']:
443 pathname = os.path.join(rootdir, 'etc', 'udev', 'rules.d', x)
444 if os.path.exists(pathname):
445 logging.debug('rm %s' % pathname)
446 os.remove(pathname)
447 else:
448 logging.debug('not removing non-existent %s' % pathname)
449
450 def setup_networking(self, rootdir):
451 self.message('Setting up networking')
452
453 f = open(os.path.join(rootdir, 'etc', 'network', 'interfaces'), 'w')
454 f.write('auto lo\n')
455 f.write('iface lo inet loopback\n')
456
457 if self.settings['enable-dhcp']:
458 f.write('\n')
459 f.write('auto eth0\n')
460 f.write('iface eth0 inet dhcp\n')
461
462 f.close()
463
464 def append_serial_console(self, rootdir):
465 if self.settings['serial-console']:
466 serial_command = self.settings['serial-console-command']
467 logging.debug('adding getty to serial console')
468 inittab = os.path.join(rootdir, 'etc/inittab')
469 with open(inittab, 'a') as f:
470 f.write('\nS0:23:respawn:%s\n' % serial_command)
471
472 def install_grub2(self, rootdev, rootdir):
473 self.message("Configuring grub2")
474 # rely on kpartx using consistent naming to map loop0p1 to loop0
475 install_dev = os.path.join('/dev', os.path.basename(rootdev)[:-2])
476 self.runcmd(['mount', '/dev', '-t', 'devfs', '-obind',
477 '%s' % os.path.join(rootdir, 'dev')])
478 self.runcmd(['mount', '/proc', '-t', 'proc', '-obind',
479 '%s' % os.path.join(rootdir, 'proc')])
480 self.runcmd(['mount', '/sys', '-t', 'sysfs', '-obind',
481 '%s' % os.path.join(rootdir, 'sys')])
482 try:
483 self.runcmd(['chroot', rootdir, 'update-grub'])
484 self.runcmd(['chroot', rootdir, 'grub-install', install_dev])
485 except cliapp.AppException as e:
486 self.message("Failed to configure grub2. Using extlinux.")
487 self.runcmd(['umount', os.path.join(rootdir, 'sys')])
488 self.runcmd(['umount', os.path.join(rootdir, 'proc')])
489 self.runcmd(['umount', os.path.join(rootdir, 'dev')])
490 self.install_extlinux(rootdev, rootdir)
491
492 def install_extlinux(self, rootdev, rootdir):
493 if not os.path.exists("/usr/bin/extlinux"):
494 self.message("extlinux not installed, skipping.")
495 return
496 self.message('Installing extlinux')
497
498 def find(pattern):
499 dirname = os.path.join(rootdir, 'boot')
500 basenames = os.listdir(dirname)
501 logging.debug('find: %s' % basenames)
502 for basename in basenames:
503 if re.search(pattern, basename):
504 return os.path.join('boot', basename)
505 raise cliapp.AppException('Cannot find match: %s' % pattern)
506
507 try:
508 kernel_image = find('vmlinuz-.*')
509 initrd_image = find('initrd.img-.*')
510 except cliapp.AppException as e:
511 self.message("Unable to find kernel. Not installing extlinux.")
512 logging.debug("No kernel found. %s. Skipping install of extlinux." % e)
513 return
514
515 out = self.runcmd(['blkid', '-c', '/dev/null', '-o', 'value',
516 '-s', 'UUID', rootdev])
517 uuid = out.splitlines()[0].strip()
518
519 conf = os.path.join(rootdir, 'extlinux.conf')
520 logging.debug('configure extlinux %s' % conf)
521 f = open(conf, 'w')
522 f.write('''
523 default linux
524 timeout 1
525
526 label linux
527 kernel %(kernel)s
528 append initrd=%(initrd)s root=UUID=%(uuid)s ro %(kserial)s
529 %(extserial)s
530 ''' % {
531 'kernel': kernel_image,
532 'initrd': initrd_image,
533 'uuid': uuid,
534 'kserial':
535 'console=ttyS0,115200' if self.settings['serial-console'] else '',
536 'extserial': 'serial 0 115200' if self.settings['serial-console'] else '',
537 })
538 f.close()
539
540 self.runcmd(['extlinux', '--install', rootdir])
541 self.runcmd(['sync'])
542 time.sleep(2)
543
544 def optimize_image(self, rootdir):
545 """
546 Filing up the image with zeros will increase its compression rate
547 """
548 zeros = os.path.join(rootdir, 'ZEROS')
549 self.runcmd_unchecked(['dd', 'if=/dev/zero', 'of=' + zeros, 'bs=1M'])
550 self.runcmd(['rm', '-f', zeros])
551
552 def squash(self):
553 """
554 Run squashfs on the image.
555 """
556 if not os.path.exists('/usr/bin/mksquashfs'):
557 logging.warning("Squash selected but mksquashfs not found!")
558 return
559 self.message("Running mksquashfs")
560 suffixed = "%s.squashfs" % self.settings['image']
561 self.runcmd(['mksquashfs', self.settings['image'],
562 suffixed,
563 '-no-progress', '-comp', 'xz'], ignore_fail=False)
564 os.unlink(self.settings['image'])
565 self.settings['image'] = suffixed
566
567 def cleanup_system(self):
568 # Clean up after any errors.
569
570 self.message('Cleaning up')
571
572 # Umount in the reverse mount order
573 if self.settings['image']:
574 for i in xrange(len(self.mount_points) - 1, -1, -1):
575 mount_point = self.mount_points[i]
576 try:
577 self.runcmd(['umount', mount_point], ignore_fail=False)
578 except cliapp.AppException:
579 logging.debug("umount failed, sleeping and trying again")
580 time.sleep(5)
581 self.runcmd(['umount', mount_point], ignore_fail=False)
582
583 self.runcmd(['kpartx', '-d', self.settings['image']], ignore_fail=True)
584
585 for dirname in self.remove_dirs:
586 shutil.rmtree(dirname)
587
588 def customize(self, rootdir):
589 script = self.settings['customize']
590 if not script:
591 return
592 if not os.path.exists(script):
593 example = os.path.join("/usr/share/vmdebootstrap/examples/", script)
594 if not os.path.exists(example):
595 self.message("Unable to find %s" % script)
596 return
597 script = example
598 self.message('Running customize script %s' % script)
599 with open('/dev/tty', 'w') as tty:
600 cliapp.runcmd([script, rootdir], stdout=tty, stderr=tty)
601
602 def create_tarball(self, rootdir):
603 # Create a tarball of the disk's contents
604 # shell out to runcmd since it more easily handles rootdir
605 self.message('Creating tarball of disk contents')
606 self.runcmd(['tar', '-cf', self.settings['tarball'], '-C', rootdir, '.'])
607
608 def chown(self, rootdir):
609 # Change image owner after completed build
610 self.message("Changing owner to %s" % self.settings["owner"])
611 subprocess.call(["chown",
612 self.settings["owner"],
613 self.settings["image"]])
614
615 def configure_apt(self, rootdir):
616 # use the distribution and mirror to create an apt source
617 self.message("Configuring apt to use distribution and mirror")
618 conf = os.path.join(rootdir, 'etc', 'apt', 'sources.list.d', 'base.list')
619 logging.debug('configure apt %s' % conf)
620 f = open(conf, 'w')
621 f.write('''
622 deb %(mirror)s %(distribution)s main
623 #deb-src %(mirror)s %(distribution)s main
624 ''' % {
625 'mirror': self.settings['mirror'],
626 'distribution': self.settings['distribution']
627 })
628 f.close()
629
630 if __name__ == '__main__':
631 VmDebootstrap(version=__version__).run()