]> git.siccegge.de Git - forks/vmdebootstrap.git/blob - vmdebootstrap
Enable networking in source directory
[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 datetime
26 import subprocess
27 import tempfile
28 import time
29 from distro_info import DebianDistroInfo, UbuntuDistroInfo
30
31
32 __version__ = '0.8'
33
34 # pylint: disable=invalid-name,line-too-long,missing-docstring,too-many-branches
35
36
37 class VmDebootstrap(cliapp.Application): # pylint: disable=too-many-public-methods
38
39 def __init__(self, progname=None, version=__version__, description=None, epilog=None):
40 super(VmDebootstrap, self).__init__(progname, version, description, epilog)
41 self.remove_dirs = []
42 self.mount_points = []
43 self.debian_info = DebianDistroInfo()
44 self.ubuntu_info = UbuntuDistroInfo()
45
46 def add_settings(self):
47 default_arch = subprocess.check_output(
48 ["dpkg", "--print-architecture"]).strip()
49
50 self.settings.boolean(
51 ['verbose'], 'report what is going on')
52 self.settings.string(
53 ['image'], 'put created disk image in FILE',
54 metavar='FILE')
55 self.settings.bytesize(
56 ['size'],
57 'create a disk image of size SIZE (%default)',
58 metavar='SIZE',
59 default='1G')
60 self.settings.bytesize(
61 ['bootsize'],
62 'create boot partition of size SIZE (%default)',
63 metavar='BOOTSIZE',
64 default='0%')
65 self.settings.string(
66 ['boottype'],
67 'specify file system type for /boot/',
68 default='ext2')
69 self.settings.bytesize(
70 ['bootoffset'],
71 'Space to leave at start of the image for bootloader',
72 default='0')
73 self.settings.string(
74 ['part-type'],
75 'Partition type to use for this image',
76 default='msdos')
77 self.settings.string(
78 ['roottype'],
79 'specify file system type for /',
80 default='ext4')
81 self.settings.bytesize(
82 ['swap'],
83 'create swap space of size SIZE (min 256Mb)')
84 self.settings.string(
85 ['foreign'],
86 'set up foreign debootstrap environment using provided program (ie binfmt handler)')
87 self.settings.string(
88 ['variant'],
89 'select debootstrap variant it not using the default')
90 self.settings.boolean(
91 ['extlinux'],
92 'install extlinux?',
93 default=True)
94 self.settings.string(
95 ['tarball'],
96 "tar up the disk's contents in FILE",
97 metavar='FILE')
98 self.settings.string(
99 ['apt-mirror'],
100 'configure apt to use MIRROR',
101 metavar='URL')
102 self.settings.string(
103 ['mirror'],
104 'use MIRROR as package source (%default)',
105 metavar='URL',
106 default='http://http.debian.net/debian/')
107 self.settings.string(
108 ['arch'],
109 'architecture to use (%default)',
110 metavar='ARCH',
111 default=default_arch)
112 self.settings.string(
113 ['distribution'],
114 'release to use (%default)',
115 metavar='NAME',
116 default='stable')
117 self.settings.string_list(
118 ['package'],
119 'install PACKAGE onto system')
120 self.settings.string_list(
121 ['custom-package'],
122 'install package in DEB file onto system (not from mirror)',
123 metavar='DEB')
124 self.settings.boolean(
125 ['no-kernel'],
126 'do not install a linux package')
127 self.settings.string(
128 ['kernel-package'],
129 'install PACKAGE instead of the default kernel package',
130 metavar='PACKAGE')
131 self.settings.boolean(
132 ['enable-dhcp'],
133 'enable DHCP on eth0')
134 self.settings.string(
135 ['root-password'],
136 'set root password',
137 metavar='PASSWORD')
138 self.settings.boolean(
139 ['lock-root-password'],
140 'lock root account so they cannot login?')
141 self.settings.string(
142 ['customize'],
143 'run SCRIPT after setting up system',
144 metavar='SCRIPT')
145 self.settings.string(
146 ['hostname'],
147 'set name to HOSTNAME (%default)',
148 metavar='HOSTNAME',
149 default='debian')
150 self.settings.string_list(
151 ['user'],
152 'create USER with PASSWORD',
153 metavar='USER/PASSWORD')
154 self.settings.boolean(
155 ['serial-console'],
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)',
160 metavar='COMMAND',
161 default='/sbin/getty -L ttyS0 115200 vt100')
162 self.settings.boolean(
163 ['sudo'],
164 'install sudo, and if user is created, add them to sudo group')
165 self.settings.string(
166 ['owner'],
167 'the user who will own the image when the build is complete.')
168 self.settings.boolean(
169 ['squash'],
170 'use squashfs on the final image.')
171 self.settings.boolean(
172 ['configure-apt'],
173 'Create an apt source based on the distribution and mirror selected.')
174 self.settings.boolean(
175 ['mbr'],
176 'Run install-mbr (default if extlinux used)')
177 self.settings.boolean(
178 ['grub'],
179 'Install and configure grub2 - disables extlinux.')
180 self.settings.boolean(
181 ['sparse'],
182 'Do not fill the image with zeros to keep a sparse disk image',
183 default=False)
184 self.settings.boolean(
185 ['pkglist'],
186 'Create a list of package names included in the image.')
187 self.settings.boolean(
188 ['no-acpid'],
189 'do not install the acpid package',
190 default=False)
191
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'])
204 rootdir = None
205 try:
206 rootdev = None
207 roottype = self.settings['roottype']
208 bootdev = None
209 boottype = None
210 if self.settings['image']:
211 self.create_empty_image()
212 self.partition_image()
213 if self.settings['mbr'] or self.settings['extlinux']:
214 self.install_mbr()
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)
221 if bootdev:
222 if self.settings['boottype']:
223 boottype = self.settings['boottype']
224 else:
225 boottype = 'ext2'
226 self.mkfs(bootdev, fstype=boottype)
227 bootdir = '%s/%s' % (rootdir, 'boot/')
228 os.mkdir(bootdir)
229 self.mount(bootdev, bootdir)
230 else:
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)
245
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']:
254 self.squash()
255 if self.settings['pkglist']:
256 self.list_installed_pkgs(rootdir)
257
258 if self.settings['foreign']:
259 os.unlink('%s/usr/bin/%s' %
260 (rootdir, os.path.basename(self.settings['foreign'])))
261
262 if self.settings['tarball']:
263 self.create_tarball(rootdir)
264
265 if self.settings['owner']:
266 self.chown()
267 except BaseException as e:
268 self.message('EEEK! Something bad happened...')
269 if rootdir:
270 db_log = os.path.join(rootdir, 'debootstrap', 'debootstrap.log')
271 if os.path.exists(db_log):
272 shutil.copy(db_log, os.getcwd())
273 self.message(e)
274 self.cleanup_system()
275 raise
276 else:
277 self.cleanup_system()
278
279 def message(self, msg):
280 logging.info(msg)
281 if self.settings['verbose']:
282 print msg
283
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,
288 env=env, **kwargs)
289 out, err = p.communicate(stdin)
290 if p.returncode != 0:
291 msg = 'command failed: %s\n%s\n%s' % (argv, out, err)
292 logging.error(msg)
293 if not ignore_fail:
294 raise cliapp.AppException(msg)
295 return out
296
297 def mkdtemp(self):
298 dirname = tempfile.mkdtemp()
299 self.remove_dirs.append(dirname)
300 logging.debug('mkdir %s', dirname)
301 return dirname
302
303 def mount(self, device, path=None):
304 if not path:
305 mount_point = self.mkdtemp()
306 else:
307 mount_point = path
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)
312 return mount_point
313
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'])])
319
320 def partition_image(self):
321 """
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.
326 """
327 self.message('Creating partitions')
328 self.runcmd(['parted', '-s', self.settings['image'],
329 'mklabel', self.settings['part-type']])
330 partoffset = 0
331 bootsize = 0
332 extent = '100%'
333 swap = 256 * 1024 * 1024
334 if self.settings['swap'] > 0:
335 if self.settings['swap'] > swap:
336 swap = self.settings['swap']
337 else:
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:
344 partoffset = 1
345 logging.info(
346 "Setting bootoffset %smib to allow for %s bytes",
347 partoffset, self.settings['bootoffset'])
348 else:
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:
353 partoffset = 1
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])
363 else:
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%'])
372
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'])
378
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']])
383 else:
384 msg = "mbr enabled but /sbin/install-mbr not found" \
385 " - please install the mbr package."
386 raise cliapp.AppException(msg)
387
388 def setup_kpartx(self):
389 bootindex = None
390 swapindex = None
391 out = self.runcmd(['kpartx', '-avs', self.settings['image']])
392 if self.settings['bootsize'] and self.settings['swap'] > 0:
393 bootindex = 0
394 rootindex = 1
395 swapindex = 2
396 parts = 3
397 elif self.settings['bootsize']:
398 bootindex = 0
399 rootindex = 1
400 parts = 2
401 elif self.settings['swap'] > 0:
402 rootindex = 0
403 swapindex = 1
404 parts = 2
405 else:
406 rootindex = 0
407 parts = 1
408 boot = None
409 swap = None
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
424
425 def mkfs(self, device, fstype):
426 self.message('Creating filesystem %s' % fstype)
427 self.runcmd(['mkfs', '-t', fstype, device])
428
429 def suite_to_codename(self, distro):
430 suite = self.debian_info.codename(distro, datetime.date.today())
431 if not suite:
432 return distro
433 return suite
434
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):
439 return False
440 return suite == self.debian_info.old(limit)
441
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):
446 return False
447 return suite == self.debian_info.stable(limit)
448
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))
452
453 include = self.settings['package']
454
455 if not self.settings['foreign'] and not self.settings['no-acpid']:
456 include.append('acpid')
457
458 if self.settings['grub']:
459 include.append('grub-pc')
460
461 if not self.settings['no-kernel']:
462 if self.settings['kernel-package']:
463 kernel_image = self.settings['kernel-package']
464 else:
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)):
468 kernel_arch = '486'
469 else:
470 kernel_arch = '586'
471 elif self.settings['arch'] == 'armhf':
472 kernel_arch = 'armmp'
473 else:
474 kernel_arch = self.settings['arch']
475 kernel_image = 'linux-image-%s' % kernel_arch
476 include.append(kernel_image)
477
478 if self.settings['sudo'] and 'sudo' not in include:
479 include.append('sudo')
480
481 args = ['debootstrap', '--arch=%s' % self.settings['arch']]
482
483 if self.settings['package']:
484 args.append(
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))
494 self.runcmd(args)
495 if self.settings['foreign']:
496 # set a noninteractive debconf environment for secondstage
497 env = {
498 "DEBIAN_FRONTEND": "noninteractive",
499 "DEBCONF_NONINTERACTIVE_SEEN": "true",
500 "LC_ALL": "C"
501 }
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'],
511 env=env)
512
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)
517
518 etc_hosts = os.path.join(rootdir, 'etc', 'hosts')
519 try:
520 with open(etc_hosts, 'r') as f:
521 data = f.read()
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)
527 except IOError:
528 pass
529
530 def create_fstab(self, rootdir, rootdev, roottype, bootdev, boottype): # pylint: disable=too-many-arguments
531 def fsuuid(device):
532 out = self.runcmd(['blkid', '-c', '/dev/null', '-o', 'value',
533 '-s', 'UUID', device])
534 return out.splitlines()[0].strip()
535
536 if rootdev:
537 rootdevstr = 'UUID=%s' % fsuuid(rootdev)
538 else:
539 rootdevstr = '/dev/sda1'
540
541 if bootdev:
542 bootdevstr = 'UUID=%s' % fsuuid(bootdev)
543 else:
544 bootdevstr = None
545
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))
550 if bootdevstr:
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")
556
557 def install_debs(self, rootdir):
558 if not self.settings['custom-package']:
559 return
560 self.message('Installing custom packages')
561 tmp = os.path.join(rootdir, 'tmp', 'install_debs')
562 os.mkdir(tmp)
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']]
567 out, err, _ = \
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)
574 shutil.rmtree(tmp)
575
576 def cleanup_apt_cache(self, rootdir):
577 out = self.runcmd(['chroot', rootdir, 'apt-get', 'clean'])
578 logging.debug('stdout:\n%s', out)
579
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'])
587 else:
588 self.message('Give root an empty password')
589 self.delete_password(rootdir, 'root')
590
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'])
597
598 for userpass in self.settings['user']:
599 if '/' in userpass:
600 user, password = userpass.split('/', 1)
601 create_user(user)
602 self.set_password(rootdir, user, password)
603 else:
604 create_user(userpass)
605 self.delete_password(rootdir, userpass)
606
607 def set_password(self, rootdir, user, password):
608 encrypted = crypt.crypt(password, '..')
609 self.runcmd(['chroot', rootdir, 'usermod', '-p', encrypted, user])
610
611 def delete_password(self, rootdir, user):
612 self.runcmd(['chroot', rootdir, 'passwd', '-d', user])
613
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)
620 os.remove(pathname)
621 else:
622 logging.debug('not removing non-existent %s', pathname)
623
624 def setup_networking(self, rootdir):
625 self.message('Setting up networking')
626
627 if not os.path.exists(os.path.join(rootdir, 'etc', 'network', 'interfaces')):
628 with open(os.path.join(
629 rootdir, 'etc', 'network', 'interfaces'), 'w') as netfile:
630 netfile.write('source-directory /etc/network/interfaces.d\n')
631
632 with open(os.path.join(
633 rootdir, 'etc', 'network', 'interfaces.d', 'setup'), 'w') as eth:
634 eth.write('auto lo\n')
635 eth.write('iface lo inet loopback\n')
636
637 if self.settings['enable-dhcp']:
638 eth.write('\n')
639 eth.write('auto eth0\n')
640 eth.write('iface eth0 inet dhcp\n')
641
642 def append_serial_console(self, rootdir):
643 if self.settings['serial-console']:
644 serial_command = self.settings['serial-console-command']
645 logging.debug('adding getty to serial console')
646 inittab = os.path.join(rootdir, 'etc/inittab')
647 # to autologin, serial_command can contain '-a root'
648 with open(inittab, 'a') as f:
649 f.write('\nS0:23:respawn:%s\n' % serial_command)
650
651 # pylint: disable=no-self-use
652 def _grub_serial_console(self, rootdir):
653 cmdline = 'GRUB_CMDLINE_LINUX_DEFAULT="console=tty0 console=tty1 console=ttyS0,38400n8"'
654 terminal = 'GRUB_TERMINAL="serial gfxterm"'
655 command = 'GRUB_SERIAL_COMMAND="serial --speed=38400 --unit=0 --parity=no --stop=1"'
656 grub_cfg = os.path.join(rootdir, 'etc', 'default', 'grub')
657 logging.debug("Allowing serial output in grub config %s", grub_cfg)
658 with open(grub_cfg, 'a+') as cfg:
659 cfg.write("# %s serial support\n" % os.path.basename(__file__))
660 cfg.write("%s\n" % cmdline)
661 cfg.write("%s\n" % terminal)
662 cfg.write("%s\n" % command)
663
664 def install_grub2(self, rootdev, rootdir):
665 self.message("Configuring grub2")
666 # rely on kpartx using consistent naming to map loop0p1 to loop0
667 install_dev = os.path.join('/dev', os.path.basename(rootdev)[:-2])
668 self.runcmd(['mount', '/dev', '-t', 'devfs', '-obind',
669 '%s' % os.path.join(rootdir, 'dev')])
670 self.runcmd(['mount', '/proc', '-t', 'proc', '-obind',
671 '%s' % os.path.join(rootdir, 'proc')])
672 self.runcmd(['mount', '/sys', '-t', 'sysfs', '-obind',
673 '%s' % os.path.join(rootdir, 'sys')])
674 if self.settings['serial-console']:
675 self._grub_serial_console(rootdir)
676
677 try:
678 self.runcmd(['chroot', rootdir, 'update-grub'])
679 self.runcmd(['chroot', rootdir, 'grub-install', install_dev])
680 except cliapp.AppException:
681 self.message("Failed. Is grub2-common installed? Using extlinux.")
682 self.install_extlinux(rootdev, rootdir)
683 self.runcmd(['umount', os.path.join(rootdir, 'sys')])
684 self.runcmd(['umount', os.path.join(rootdir, 'proc')])
685 self.runcmd(['umount', os.path.join(rootdir, 'dev')])
686
687 def install_extlinux(self, rootdev, rootdir):
688 if not os.path.exists("/usr/bin/extlinux"):
689 self.message("extlinux not installed, skipping.")
690 return
691 self.message('Installing extlinux')
692
693 def find(pattern):
694 dirname = os.path.join(rootdir, 'boot')
695 basenames = os.listdir(dirname)
696 logging.debug('find: %s', basenames)
697 for basename in basenames:
698 if re.search(pattern, basename):
699 return os.path.join('boot', basename)
700 raise cliapp.AppException('Cannot find match: %s' % pattern)
701
702 try:
703 kernel_image = find('vmlinuz-.*')
704 initrd_image = find('initrd.img-.*')
705 except cliapp.AppException as e:
706 self.message("Unable to find kernel. Not installing extlinux.")
707 logging.debug("No kernel found. %s. Skipping install of extlinux.", e)
708 return
709
710 out = self.runcmd(['blkid', '-c', '/dev/null', '-o', 'value',
711 '-s', 'UUID', rootdev])
712 uuid = out.splitlines()[0].strip()
713
714 conf = os.path.join(rootdir, 'extlinux.conf')
715 logging.debug('configure extlinux %s', conf)
716 kserial = 'console=ttyS0,115200' if self.settings['serial-console'] else ''
717 extserial = 'serial 0 115200' if self.settings['serial-console'] else ''
718 msg = '''
719 default linux
720 timeout 1
721
722 label linux
723 kernel %(kernel)s
724 append initrd=%(initrd)s root=UUID=%(uuid)s ro %(kserial)s
725 %(extserial)s
726 ''' % {
727 'kernel': kernel_image, # pylint: disable=bad-continuation
728 'initrd': initrd_image, # pylint: disable=bad-continuation
729 'uuid': uuid, # pylint: disable=bad-continuation
730 'kserial': kserial, # pylint: disable=bad-continuation
731 'extserial': extserial, # pylint: disable=bad-continuation
732 } # pylint: disable=bad-continuation
733 logging.debug("extlinux config:\n%s", msg)
734
735 # python multiline string substitution is just ugly.
736 # use an external file or live with the mangling, no point in
737 # mangling the string to remove spaces just to keep it pretty in source.
738 f = open(conf, 'w')
739 f.write(msg)
740
741 self.runcmd(['extlinux', '--install', rootdir])
742 self.runcmd(['sync'])
743 time.sleep(2)
744
745 def optimize_image(self, rootdir):
746 """
747 Filing up the image with zeros will increase its compression rate
748 """
749 if not self.settings['sparse']:
750 zeros = os.path.join(rootdir, 'ZEROS')
751 self.runcmd_unchecked(['dd', 'if=/dev/zero', 'of=' + zeros, 'bs=1M'])
752 self.runcmd(['rm', '-f', zeros])
753
754 def squash(self):
755 """
756 Run squashfs on the image.
757 """
758 if not os.path.exists('/usr/bin/mksquashfs'):
759 logging.warning("Squash selected but mksquashfs not found!")
760 return
761 self.message("Running mksquashfs")
762 suffixed = "%s.squashfs" % self.settings['image']
763 self.runcmd(['mksquashfs', self.settings['image'],
764 suffixed,
765 '-no-progress', '-comp', 'xz'], ignore_fail=False)
766 os.unlink(self.settings['image'])
767 self.settings['image'] = suffixed
768
769 def cleanup_system(self):
770 # Clean up after any errors.
771
772 self.message('Cleaning up')
773
774 # Umount in the reverse mount order
775 if self.settings['image']:
776 for i in range(len(self.mount_points) - 1, -1, -1):
777 mount_point = self.mount_points[i]
778 try:
779 self.runcmd(['umount', mount_point], ignore_fail=False)
780 except cliapp.AppException:
781 logging.debug("umount failed, sleeping and trying again")
782 time.sleep(5)
783 self.runcmd(['umount', mount_point], ignore_fail=False)
784
785 self.runcmd(['kpartx', '-d', self.settings['image']], ignore_fail=True)
786
787 for dirname in self.remove_dirs:
788 shutil.rmtree(dirname)
789
790 def customize(self, rootdir):
791 script = self.settings['customize']
792 if not script:
793 return
794 if not os.path.exists(script):
795 example = os.path.join("/usr/share/vmdebootstrap/examples/", script)
796 if not os.path.exists(example):
797 self.message("Unable to find %s" % script)
798 return
799 script = example
800 self.message('Running customize script %s' % script)
801 logging.info("rootdir=%s image=%s", rootdir, self.settings['image'])
802 with open('/dev/tty', 'w') as tty:
803 try:
804 cliapp.runcmd([script, rootdir, self.settings['image']], stdout=tty, stderr=tty)
805 except IOError:
806 subprocess.call([script, rootdir, self.settings['image']])
807
808 def create_tarball(self, rootdir):
809 # Create a tarball of the disk's contents
810 # shell out to runcmd since it more easily handles rootdir
811 self.message('Creating tarball of disk contents')
812 self.runcmd(['tar', '-cf', self.settings['tarball'], '-C', rootdir, '.'])
813
814 def chown(self):
815 # Change image owner after completed build
816 if self.settings['image']:
817 filename = self.settings['image']
818 elif self.settings['tarball']:
819 filename = self.settings['tarball']
820 else:
821 return
822 self.message("Changing owner to %s" % self.settings["owner"])
823 subprocess.call(["chown", self.settings["owner"], filename])
824
825 def list_installed_pkgs(self, rootdir):
826 # output the list of installed packages for sources identification
827 self.message("Creating a list of installed binary package names")
828 out = self.runcmd(['chroot', rootdir,
829 'dpkg-query', '-W', "-f='${Package}.deb\n'"])
830 with open('dpkg.list', 'w') as dpkg:
831 dpkg.write(out)
832
833 def configure_apt(self, rootdir):
834 # use the distribution and mirror to create an apt source
835 self.message("Configuring apt to use distribution and mirror")
836 conf = os.path.join(rootdir, 'etc', 'apt', 'sources.list.d', 'base.list')
837 logging.debug('configure apt %s', conf)
838 mirror = self.settings['mirror']
839 if self.settings['apt-mirror']:
840 mirror = self.settings['apt-mirror']
841 self.message("Setting apt mirror to %s" % mirror)
842 os.unlink(os.path.join(rootdir, 'etc', 'apt', 'sources.list'))
843 f = open(conf, 'w')
844 line = 'deb %s %s main\n' % (mirror, self.settings['distribution'])
845 f.write(line)
846 line = '#deb-src %s %s main\n' % (mirror, self.settings['distribution'])
847 f.write(line)
848 f.close()
849 # ensure the apt sources have valid lists
850 self.runcmd(['chroot', rootdir, 'apt-get', '-qq', 'update'])
851
852 if __name__ == '__main__':
853 VmDebootstrap(version=__version__).run()